// Copyright 2017 Google
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#import <Foundation/Foundation.h>

#import "Private/FIRReachabilityChecker.h"
#import "Private/FIRReachabilityChecker+Internal.h"

#import "Private/FIRNetwork.h"
#import "Private/FIRNetworkMessageCode.h"
#import "Private/FIRLogger.h"

static void ReachabilityCallback(SCNetworkReachabilityRef reachability,
                                 SCNetworkReachabilityFlags flags, void *info);

static const struct FIRReachabilityApi kFIRDefaultReachabilityApi = {
    SCNetworkReachabilityCreateWithName,
    SCNetworkReachabilitySetCallback,
    SCNetworkReachabilityScheduleWithRunLoop,
    SCNetworkReachabilityUnscheduleFromRunLoop,
    CFRelease,
};

static NSString *const kFIRReachabilityUnknownStatus = @"Unknown";
static NSString *const kFIRReachabilityConnectedStatus = @"Connected";
static NSString *const kFIRReachabilityDisconnectedStatus = @"Disconnected";

@interface FIRReachabilityChecker ()

@property(nonatomic, assign) const struct FIRReachabilityApi *reachabilityApi;
@property(nonatomic, assign) FIRReachabilityStatus reachabilityStatus;
@property(nonatomic, copy) NSString *host;
@property(nonatomic, assign) SCNetworkReachabilityRef reachability;

@end

@implementation FIRReachabilityChecker

@synthesize reachabilityApi = reachabilityApi_;
@synthesize reachability = reachability_;

- (const struct FIRReachabilityApi *)reachabilityApi {
  return reachabilityApi_;
}

- (void)setReachabilityApi:(const struct FIRReachabilityApi *)reachabilityApi {
  if (reachability_) {
    NSString *message = @"Cannot change reachability API while reachability is running. "
                        @"Call stop first.";
    [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelError
                                 messageCode:kFIRNetworkMessageCodeReachabilityChecker000
                                     message:message];
    return;
  }
  reachabilityApi_ = reachabilityApi;
}

@synthesize reachabilityStatus = reachabilityStatus_;
@synthesize host = host_;
@synthesize reachabilityDelegate = reachabilityDelegate_;
@synthesize loggerDelegate = loggerDelegate_;

- (BOOL)isActive {
  return reachability_ != nil;
}

- (void)setReachabilityDelegate:(id<FIRReachabilityDelegate>)reachabilityDelegate {
  if (reachabilityDelegate &&
      (![(NSObject *)reachabilityDelegate conformsToProtocol:@protocol(FIRReachabilityDelegate)])) {
    FIRLogError(
        kFIRLoggerCore,
        [NSString stringWithFormat:@"I-NET%06ld",
        (long)kFIRNetworkMessageCodeReachabilityChecker005],
        @"Reachability delegate doesn't conform to Reachability protocol.");
    return;
  }
  reachabilityDelegate_ = reachabilityDelegate;
}

- (void)setLoggerDelegate:(id<FIRNetworkLoggerDelegate>)loggerDelegate {
  if (loggerDelegate &&
      (![(NSObject *)loggerDelegate conformsToProtocol:@protocol(FIRNetworkLoggerDelegate)])) {
    FIRLogError(
        kFIRLoggerCore,
        [NSString stringWithFormat:@"I-NET%06ld",
        (long)kFIRNetworkMessageCodeReachabilityChecker006],
        @"Reachability delegate doesn't conform to Logger protocol.");
    return;
  }
  loggerDelegate_ = loggerDelegate;
}

- (instancetype)initWithReachabilityDelegate:(id<FIRReachabilityDelegate>)reachabilityDelegate
                              loggerDelegate:(id<FIRNetworkLoggerDelegate>)loggerDelegate
                                    withHost:(NSString *)host {
  self = [super init];

  [self setLoggerDelegate:loggerDelegate];

  if (!host || !host.length) {
    [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelError
                                 messageCode:kFIRNetworkMessageCodeReachabilityChecker001
                                     message:@"Invalid host specified"];
    return nil;
  }
  if (self) {
    [self setReachabilityDelegate:reachabilityDelegate];
    reachabilityApi_ = &kFIRDefaultReachabilityApi;
    reachabilityStatus_ = kFIRReachabilityUnknown;
    host_ = [host copy];
    reachability_ = nil;
  }
  return self;
}

- (void)dealloc {
  reachabilityDelegate_ = nil;
  loggerDelegate_ = nil;
  [self stop];
}

- (BOOL)start {
  if (!reachability_) {
    reachability_ = reachabilityApi_->createWithNameFn(kCFAllocatorDefault, [host_ UTF8String]);
    if (!reachability_) {
      return NO;
    }
    SCNetworkReachabilityContext context = {
        0, /* version */
        (__bridge void *)(self), /* info (passed as last parameter to reachability callback) */
        NULL, /* retain */
        NULL, /* release */
        NULL /* copyDescription */
    };
    if (!reachabilityApi_->setCallbackFn(reachability_, ReachabilityCallback, &context) ||
        !reachabilityApi_->scheduleWithRunLoopFn(reachability_, CFRunLoopGetMain(),
                                                 kCFRunLoopCommonModes)) {
      reachabilityApi_->releaseFn(reachability_);
      reachability_ = nil;
      [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelError
                                   messageCode:kFIRNetworkMessageCodeReachabilityChecker002
                                       message:@"Failed to start reachability handle"];
      return NO;
    }
  }
  [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
                               messageCode:kFIRNetworkMessageCodeReachabilityChecker003
                                   message:@"Monitoring the network status"];
  return YES;
}

- (void)stop {
  if (reachability_) {
    reachabilityStatus_ = kFIRReachabilityUnknown;
    reachabilityApi_->unscheduleFromRunLoopFn(reachability_, CFRunLoopGetMain(),
                                              kCFRunLoopCommonModes);
    reachabilityApi_->releaseFn(reachability_);
    reachability_ = nil;
  }
}

- (FIRReachabilityStatus)statusForFlags:(SCNetworkReachabilityFlags)flags {
  FIRReachabilityStatus status = kFIRReachabilityNotReachable;
  // If the Reachable flag is not set, we definitely don't have connectivity.
  if (flags & kSCNetworkReachabilityFlagsReachable) {
    // Reachable flag is set. Check further flags.
    if (!(flags & kSCNetworkReachabilityFlagsConnectionRequired)) {
      // Connection required flag is not set, so we have connectivity.
      #if TARGET_OS_IOS
      status = (flags & kSCNetworkReachabilityFlagsIsWWAN) ? kFIRReachabilityViaCellular
                                                           : kFIRReachabilityViaWifi;
      #elif TARGET_OS_OSX
      status = kFIRReachabilityViaWifi;
      #endif
    } else if ((flags & (kSCNetworkReachabilityFlagsConnectionOnDemand |
                         kSCNetworkReachabilityFlagsConnectionOnTraffic)) &&
               !(flags & kSCNetworkReachabilityFlagsInterventionRequired)) {
      // If the connection on demand or connection on traffic flag is set, and user intervention
      // is not required, we have connectivity.
      #if TARGET_OS_IOS
      status = (flags & kSCNetworkReachabilityFlagsIsWWAN) ? kFIRReachabilityViaCellular
                                                           : kFIRReachabilityViaWifi;
      #elif TARGET_OS_OSX
      status = kFIRReachabilityViaWifi;
      #endif
    }
  }
  return status;
}

- (void)reachabilityFlagsChanged:(SCNetworkReachabilityFlags)flags {
  FIRReachabilityStatus status = [self statusForFlags:flags];
  if (reachabilityStatus_ != status) {
    NSString *reachabilityStatusString;
    if (status == kFIRReachabilityUnknown) {
      reachabilityStatusString = kFIRReachabilityUnknownStatus;
    } else {
      reachabilityStatusString = (status == kFIRReachabilityNotReachable)
                                     ? kFIRReachabilityDisconnectedStatus
                                     : kFIRReachabilityConnectedStatus;
    }
    [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelDebug
                                 messageCode:kFIRNetworkMessageCodeReachabilityChecker004
                                     message:@"Network status has changed. Code, status"
                                    contexts:@[ @(status), reachabilityStatusString ]];
    reachabilityStatus_ = status;
    [reachabilityDelegate_ reachability:self statusChanged:reachabilityStatus_];
  }
}

@end

static void ReachabilityCallback(SCNetworkReachabilityRef reachability,
                                 SCNetworkReachabilityFlags flags, void *info) {
  FIRReachabilityChecker *checker = (__bridge FIRReachabilityChecker *)info;
  [checker reachabilityFlagsChanged:flags];
}

// This function used to be at the top of the file, but it was moved here
// as a workaround for a suspected compiler bug. When compiled in Release mode
// and run on an iOS device with WiFi disabled, the reachability code crashed
// when calling SCNetworkReachabilityScheduleWithRunLoop, or shortly thereafter.
// After unsuccessfully trying to diagnose the cause of the crash, it was
// discovered that moving this function to the end of the file magically fixed
// the crash. If you are going to edit this file, exercise caution and make sure
// to test thoroughly with an iOS device under various network conditions.
const NSString *FIRReachabilityStatusString(FIRReachabilityStatus status) {
  switch (status) {
    case kFIRReachabilityUnknown:
      return @"Reachability Unknown";

    case kFIRReachabilityNotReachable:
      return @"Not reachable";

    case kFIRReachabilityViaWifi:
      return @"Reachable via Wifi";

    case kFIRReachabilityViaCellular:
      return @"Reachable via Cellular Data";

    default:
      return [NSString stringWithFormat:@"Invalid reachability status %d", (int)status];
  }
}
