/*
 *
 * Copyright 2016 gRPC authors.
 *
 * 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 "GRPCConnectivityMonitor.h"

#pragma mark Flags

@implementation GRPCReachabilityFlags {
  SCNetworkReachabilityFlags _flags;
}

+ (instancetype)flagsWithFlags:(SCNetworkReachabilityFlags)flags {
  return [[self alloc] initWithFlags:flags];
}

- (instancetype)initWithFlags:(SCNetworkReachabilityFlags)flags {
  if ((self = [super init])) {
    _flags = flags;
  }
  return self;
}

/*
 * One accessor method implementation per flag. Example:

- (BOOL)isCell { \
  return !!(_flags & kSCNetworkReachabilityFlagsIsWWAN); \
}

 */
#define GRPC_XMACRO_ITEM(methodName, FlagName) \
- (BOOL)methodName { \
  return !!(_flags & kSCNetworkReachabilityFlags ## FlagName); \
}
#include "GRPCReachabilityFlagNames.xmacro.h"
#undef GRPC_XMACRO_ITEM

- (BOOL)isHostReachable {
  // Note: connectionOnDemand means it'll be reachable only if using the CFSocketStream API or APIs
  // on top of it.
  // connectionRequired means we can't tell until a connection is attempted (e.g. for VPN on
  // demand).
  return self.reachable && !self.interventionRequired && !self.connectionOnDemand;
}

- (NSString *)description {
  NSMutableArray *activeOptions = [NSMutableArray arrayWithCapacity:9];

  /*
   * For each flag, add its name to the array if it's ON. Example:

  if (self.isCell) {
    [activeOptions addObject:@"isCell"];
  }

   */
  #define GRPC_XMACRO_ITEM(methodName, FlagName) \
    if (self.methodName) {                       \
      [activeOptions addObject:@ #methodName];   \
    }
  #include "GRPCReachabilityFlagNames.xmacro.h"
  #undef GRPC_XMACRO_ITEM

  return activeOptions.count == 0 ? @"(none)" : [activeOptions componentsJoinedByString:@", "];
}

- (BOOL)isEqual:(id)object {
  return [object isKindOfClass:[GRPCReachabilityFlags class]] &&
      _flags == ((GRPCReachabilityFlags *)object)->_flags;
}

- (NSUInteger)hash {
  return _flags;
}
@end

#pragma mark Connectivity Monitor

// Assumes the third argument is a block that accepts a GRPCReachabilityFlags object, and passes the
// received ones to it.
static void PassFlagsToContextInfoBlock(SCNetworkReachabilityRef target,
                                        SCNetworkReachabilityFlags flags,
                                        void *info) {
  #pragma unused (target)
  // This can be called many times with the same info. The info is retained by SCNetworkReachability
  // while this function is being executed.
  void (^handler)(GRPCReachabilityFlags *) = (__bridge void (^)(GRPCReachabilityFlags *))info;
  handler([[GRPCReachabilityFlags alloc] initWithFlags:flags]);
}

@implementation GRPCConnectivityMonitor {
  SCNetworkReachabilityRef _reachabilityRef;
  GRPCReachabilityFlags *_previousReachabilityFlags;
}

- (nullable instancetype)initWithReachability:(nullable SCNetworkReachabilityRef)reachability {
  if (!reachability) {
    return nil;
  }
  if ((self = [super init])) {
    _reachabilityRef = CFRetain(reachability);
    _queue = dispatch_get_main_queue();
    _previousReachabilityFlags = nil;
  }
  return self;
}

+ (nullable instancetype)monitorWithHost:(nonnull NSString *)host {
  const char *hostName = host.UTF8String;
  if (!hostName) {
    [NSException raise:NSInvalidArgumentException
                format:@"host.UTF8String returns NULL for %@", host];
  }
  SCNetworkReachabilityRef reachability =
      SCNetworkReachabilityCreateWithName(NULL, hostName);

  GRPCConnectivityMonitor *returnValue = [[self alloc] initWithReachability:reachability];
  if (reachability) {
    CFRelease(reachability);
  }
  return returnValue;
}

- (void)handleLossWithHandler:(nullable void (^)())lossHandler
      wifiStatusChangeHandler:(nullable void (^)())wifiStatusChangeHandler {
  __weak typeof(self) weakSelf = self;
  [self startListeningWithHandler:^(GRPCReachabilityFlags *flags) {
    typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
      if (lossHandler && !flags.reachable) {
        lossHandler();
#if TARGET_OS_IPHONE
      } else if (wifiStatusChangeHandler &&
                 strongSelf->_previousReachabilityFlags &&
                 (flags.isWWAN ^
                  strongSelf->_previousReachabilityFlags.isWWAN)) {
        wifiStatusChangeHandler();
#endif
      }
      strongSelf->_previousReachabilityFlags = flags;
    }
  }];
}

- (void)startListeningWithHandler:(void (^)(GRPCReachabilityFlags *))handler {
  // Copy to ensure the handler block is in the heap (and so can't be deallocated when this method
  // returns).
  void (^copiedHandler)(GRPCReachabilityFlags *) = [handler copy];
  SCNetworkReachabilityContext context = {
    .version = 0,
    .info = (__bridge void *)copiedHandler,
    .retain = CFRetain,
    .release = CFRelease,
  };
  // The following will retain context.info, and release it when the callback is set to NULL.
  SCNetworkReachabilitySetCallback(_reachabilityRef, PassFlagsToContextInfoBlock, &context);
  SCNetworkReachabilitySetDispatchQueue(_reachabilityRef, _queue);
}

- (void)stopListening {
  // This releases the block on context.info.
  SCNetworkReachabilitySetCallback(_reachabilityRef, NULL, NULL);
  SCNetworkReachabilitySetDispatchQueue(_reachabilityRef, NULL);
}

- (void)setQueue:(dispatch_queue_t)queue {
  _queue = queue ?: dispatch_get_main_queue();
}

- (void)dealloc {
  if (_reachabilityRef) {
    [self stopListening];
    CFRelease(_reachabilityRef);
  }
}

@end
