/*
 * Copyright 2019 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 "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h"

#import <sys/sysctl.h>

#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORAssert.h"
#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORReachability.h"
#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORConsoleLogger.h"

#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORRegistrar_Private.h"

#ifdef GDTCOR_VERSION
#define STR(x) STR_EXPAND(x)
#define STR_EXPAND(x) #x
NSString *const kGDTCORVersion = @STR(GDTCOR_VERSION);
#else
NSString *const kGDTCORVersion = @"Unknown";
#endif  // GDTCOR_VERSION

const GDTCORBackgroundIdentifier GDTCORBackgroundIdentifierInvalid = 0;

NSString *const kGDTCORApplicationDidEnterBackgroundNotification =
    @"GDTCORApplicationDidEnterBackgroundNotification";

NSString *const kGDTCORApplicationWillEnterForegroundNotification =
    @"GDTCORApplicationWillEnterForegroundNotification";

NSString *const kGDTCORApplicationWillTerminateNotification =
    @"GDTCORApplicationWillTerminateNotification";

NSURL *GDTCORRootDirectory(void) {
  static NSURL *GDTPath;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    NSString *cachePath =
        NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
    GDTPath =
        [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/google-sdks-events", cachePath]];
    GDTCORLogDebug(@"GDT's state will be saved to: %@", GDTPath);
  });
  NSError *error;
  [[NSFileManager defaultManager] createDirectoryAtPath:GDTPath.path
                            withIntermediateDirectories:YES
                                             attributes:nil
                                                  error:&error];
  GDTCORAssert(error == nil, @"There was an error creating GDT's path");
  return GDTPath;
}

BOOL GDTCORReachabilityFlagsReachable(GDTCORNetworkReachabilityFlags flags) {
#if !TARGET_OS_WATCH
  BOOL reachable =
      (flags & kSCNetworkReachabilityFlagsReachable) == kSCNetworkReachabilityFlagsReachable;
  BOOL connectionRequired = (flags & kSCNetworkReachabilityFlagsConnectionRequired) ==
                            kSCNetworkReachabilityFlagsConnectionRequired;
  return reachable && !connectionRequired;
#else
  return (flags & kGDTCORNetworkReachabilityFlagsReachable) ==
         kGDTCORNetworkReachabilityFlagsReachable;
#endif
}

BOOL GDTCORReachabilityFlagsContainWWAN(GDTCORNetworkReachabilityFlags flags) {
#if TARGET_OS_IOS
  return (flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN;
#else
  // Assume network connection not WWAN on macOS, tvOS, watchOS.
  return NO;
#endif  // TARGET_OS_IOS
}

GDTCORNetworkType GDTCORNetworkTypeMessage(void) {
#if !TARGET_OS_WATCH
  SCNetworkReachabilityFlags reachabilityFlags = [GDTCORReachability currentFlags];
  if ((reachabilityFlags & kSCNetworkReachabilityFlagsReachable) ==
      kSCNetworkReachabilityFlagsReachable) {
    if (GDTCORReachabilityFlagsContainWWAN(reachabilityFlags)) {
      return GDTCORNetworkTypeMobile;
    } else {
      return GDTCORNetworkTypeWIFI;
    }
  }
#endif
  return GDTCORNetworkTypeUNKNOWN;
}

GDTCORNetworkMobileSubtype GDTCORNetworkMobileSubTypeMessage(void) {
// TODO(Xcode 15): When Xcode 15 is the minimum supported Xcode version,
// it will be unnecessary to check if `TARGET_OS_VISION` is defined.
#if TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION)
  static NSDictionary<NSString *, NSNumber *> *CTRadioAccessTechnologyToNetworkSubTypeMessage;
  static CTTelephonyNetworkInfo *networkInfo;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    CTRadioAccessTechnologyToNetworkSubTypeMessage = @{
      CTRadioAccessTechnologyGPRS : @(GDTCORNetworkMobileSubtypeGPRS),
      CTRadioAccessTechnologyEdge : @(GDTCORNetworkMobileSubtypeEdge),
      CTRadioAccessTechnologyWCDMA : @(GDTCORNetworkMobileSubtypeWCDMA),
      CTRadioAccessTechnologyHSDPA : @(GDTCORNetworkMobileSubtypeHSDPA),
      CTRadioAccessTechnologyHSUPA : @(GDTCORNetworkMobileSubtypeHSUPA),
      CTRadioAccessTechnologyCDMA1x : @(GDTCORNetworkMobileSubtypeCDMA1x),
      CTRadioAccessTechnologyCDMAEVDORev0 : @(GDTCORNetworkMobileSubtypeCDMAEVDORev0),
      CTRadioAccessTechnologyCDMAEVDORevA : @(GDTCORNetworkMobileSubtypeCDMAEVDORevA),
      CTRadioAccessTechnologyCDMAEVDORevB : @(GDTCORNetworkMobileSubtypeCDMAEVDORevB),
      CTRadioAccessTechnologyeHRPD : @(GDTCORNetworkMobileSubtypeHRPD),
      CTRadioAccessTechnologyLTE : @(GDTCORNetworkMobileSubtypeLTE),
    };
    networkInfo = [[CTTelephonyNetworkInfo alloc] init];
  });
  NSString *networkCurrentRadioAccessTechnology;
#if TARGET_OS_MACCATALYST
  NSDictionary<NSString *, NSString *> *networkCurrentRadioAccessTechnologyDict =
      networkInfo.serviceCurrentRadioAccessTechnology;
  if (networkCurrentRadioAccessTechnologyDict.count) {
    networkCurrentRadioAccessTechnology = networkCurrentRadioAccessTechnologyDict.allValues[0];
  }
#else  // TARGET_OS_MACCATALYST
  if (@available(iOS 12.0, *)) {
    NSDictionary<NSString *, NSString *> *networkCurrentRadioAccessTechnologyDict =
        networkInfo.serviceCurrentRadioAccessTechnology;
    if (networkCurrentRadioAccessTechnologyDict.count) {
      // In iOS 12, multiple radio technologies can be captured. We prefer not particular radio
      // tech to another, so we'll just return the first value in the dictionary.
      networkCurrentRadioAccessTechnology = networkCurrentRadioAccessTechnologyDict.allValues[0];
    }
  } else {
#if TARGET_OS_IOS && __IPHONE_OS_VERSION_MIN_REQUIRED < 120000
    networkCurrentRadioAccessTechnology = networkInfo.currentRadioAccessTechnology;
#endif  // TARGET_OS_IOS && __IPHONE_OS_VERSION_MIN_REQUIRED < 120000
  }
#endif  // TARGET_OS_MACCATALYST
  if (networkCurrentRadioAccessTechnology) {
    NSNumber *networkMobileSubtype =
        CTRadioAccessTechnologyToNetworkSubTypeMessage[networkCurrentRadioAccessTechnology];
    return networkMobileSubtype.intValue;
  } else {
    return GDTCORNetworkMobileSubtypeUNKNOWN;
  }
#else   // TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION)
  return GDTCORNetworkMobileSubtypeUNKNOWN;
#endif  // TARGET_OS_IOS && (!defined(TARGET_OS_VISION) || !TARGET_OS_VISION)
}

NSString *_Nonnull GDTCORDeviceModel(void) {
  static NSString *deviceModel = @"Unknown";

#if TARGET_OS_IOS || TARGET_OS_TV
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    size_t size;
    char *keyToExtract = "hw.machine";
    sysctlbyname(keyToExtract, NULL, &size, NULL, 0);
    if (size > 0) {
      char *machine = calloc(1, size);
      sysctlbyname(keyToExtract, machine, &size, NULL, 0);
      deviceModel = [NSString stringWithCString:machine encoding:NSUTF8StringEncoding];
      free(machine);
    } else {
      deviceModel = [UIDevice currentDevice].model;
    }
  });
#endif

  return deviceModel;
}

NSData *_Nullable GDTCOREncodeArchive(id<NSSecureCoding> obj,
                                      NSString *filePath,
                                      NSError *_Nullable *error) {
  BOOL result = NO;
  if (filePath.length > 0) {
    // TODO(ncooke3): For future cleanup– this API shouldn't touch the file
    // system unless it successfully encoded the given object.
    result = [[NSFileManager defaultManager]
              createDirectoryAtPath:[filePath stringByDeletingLastPathComponent]
        withIntermediateDirectories:YES
                         attributes:nil
                              error:error];
    if (result == NO || *error) {
      GDTCORLogDebug(@"Attempt to create directory failed: path:%@ error:%@", filePath, *error);
      return nil;
    }
  }
  NSData *resultData;
  if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4, *)) {
    resultData = [NSKeyedArchiver archivedDataWithRootObject:obj
                                       requiringSecureCoding:YES
                                                       error:error];
    if (resultData == nil || (error != NULL && *error != nil)) {
      GDTCORLogDebug(@"Encoding an object failed: %@", *error);
      return nil;
    }
    if (filePath.length > 0) {
      result = [resultData writeToFile:filePath options:NSDataWritingAtomic error:error];
      if (result == NO || (error != NULL && *error != nil)) {
        if (error != NULL && *error != nil) {
          GDTCORLogDebug(@"Attempt to write archive failed: path:%@ error:%@", filePath, *error);
        } else {
          GDTCORLogDebug(@"Attempt to write archive failed: path:%@", filePath);
        }
      } else {
        GDTCORLogDebug(@"Writing archive succeeded: %@", filePath);
      }
    }
  } else {
    @try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
      resultData = [NSKeyedArchiver archivedDataWithRootObject:obj];
#pragma clang diagnostic pop
      if (filePath.length > 0) {
        result = [resultData writeToFile:filePath options:NSDataWritingAtomic error:error];
        if (result == NO || *error) {
          GDTCORLogDebug(@"Attempt to write archive failed: URL:%@ error:%@", filePath, *error);
        } else {
          GDTCORLogDebug(@"Writing archive succeeded: %@", filePath);
        }
      }
    } @catch (NSException *exception) {
      NSString *errorString =
          [NSString stringWithFormat:@"An exception was thrown during encoding: %@", exception];
      *error = [NSError errorWithDomain:NSCocoaErrorDomain
                                   code:-1
                               userInfo:@{NSLocalizedFailureReasonErrorKey : errorString}];
    }
    if (filePath.length > 0) {
      GDTCORLogDebug(@"Attempt to write archive. successful:%@ URL:%@ error:%@",
                     result ? @"YES" : @"NO", filePath, *error);
    }
  }
  return resultData;
}

id<NSSecureCoding> _Nullable GDTCORDecodeArchiveAtPath(Class archiveClass,
                                                       NSString *_Nonnull archivePath,
                                                       NSError **_Nonnull error) {
  NSData *data = [NSData dataWithContentsOfFile:archivePath options:0 error:error];
  if (data == nil) {
    // Reading the file failed and `error` will be populated.
    return nil;
  }

  return GDTCORDecodeArchive(archiveClass, data, error);
}

id<NSSecureCoding> _Nullable GDTCORDecodeArchive(Class archiveClass,
                                                 NSData *_Nonnull archiveData,
                                                 NSError **_Nonnull error) {
  id<NSSecureCoding> unarchivedObject = nil;
  if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4, *)) {
    unarchivedObject = [NSKeyedUnarchiver unarchivedObjectOfClass:archiveClass
                                                         fromData:archiveData
                                                            error:error];
  } else {
    @try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
      unarchivedObject = [NSKeyedUnarchiver unarchiveObjectWithData:archiveData];
#pragma clang diagnostic pop
    } @catch (NSException *exception) {
      NSString *errorString =
          [NSString stringWithFormat:@"An exception was thrown during encoding: %@", exception];
      *error = [NSError errorWithDomain:NSCocoaErrorDomain
                                   code:-1
                               userInfo:@{NSLocalizedFailureReasonErrorKey : errorString}];
    }
  }
  return unarchivedObject;
}

BOOL GDTCORWriteDataToFile(NSData *data, NSString *filePath, NSError *_Nullable *outError) {
  BOOL result = NO;
  if (filePath.length > 0) {
    result = [[NSFileManager defaultManager]
              createDirectoryAtPath:[filePath stringByDeletingLastPathComponent]
        withIntermediateDirectories:YES
                         attributes:nil
                              error:outError];
    if (result == NO || *outError) {
      GDTCORLogDebug(@"Attempt to create directory failed: path:%@ error:%@", filePath, *outError);
      return result;
    }
  }

  if (filePath.length > 0) {
    result = [data writeToFile:filePath options:NSDataWritingAtomic error:outError];
    if (result == NO || *outError) {
      GDTCORLogDebug(@"Attempt to write archive failed: path:%@ error:%@", filePath, *outError);
    } else {
      GDTCORLogDebug(@"Writing archive succeeded: %@", filePath);
    }
  }

  return result;
}

@interface GDTCORApplication ()
/**
 Private flag to match the existing `readonly` public flag. This will be accurate for all platforms,
 since we handle each platform's lifecycle notifications separately.
 */
@property(atomic, readwrite) BOOL isRunningInBackground;

@end

@implementation GDTCORApplication

#if TARGET_OS_WATCH
/** A dispatch queue on which all task semaphores will populate and remove from
 * gBackgroundIdentifierToSemaphoreMap.
 */
static dispatch_queue_t gSemaphoreQueue;

/** For mapping backgroundIdentifier to task semaphore. */
static NSMutableDictionary<NSNumber *, dispatch_semaphore_t> *gBackgroundIdentifierToSemaphoreMap;
#endif

+ (void)load {
  GDTCORLogDebug(
      @"%@", @"GDT is initializing. Please note that if you quit the app via the "
              "debugger and not through a lifecycle event, event data will remain on disk but "
              "storage won't have a reference to them since the singleton wasn't saved to disk.");
#if TARGET_OS_IOS || TARGET_OS_TV
  // If this asserts, please file a bug at https://github.com/firebase/firebase-ios-sdk/issues.
  GDTCORFatalAssert(
      GDTCORBackgroundIdentifierInvalid == UIBackgroundTaskInvalid,
      @"GDTCORBackgroundIdentifierInvalid and UIBackgroundTaskInvalid should be the same.");
#endif
  [self sharedApplication];
}

+ (void)initialize {
#if TARGET_OS_WATCH
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    gSemaphoreQueue = dispatch_queue_create("com.google.GDTCORApplication", DISPATCH_QUEUE_SERIAL);
    GDTCORLogDebug(
        @"%@",
        @"GDTCORApplication is initializing on watchOS, gSemaphoreQueue has been initialized.");
    gBackgroundIdentifierToSemaphoreMap = [[NSMutableDictionary alloc] init];
    GDTCORLogDebug(@"%@", @"GDTCORApplication is initializing on watchOS, "
                          @"gBackgroundIdentifierToSemaphoreMap has been initialized.");
  });
#endif
}

+ (nullable GDTCORApplication *)sharedApplication {
  static GDTCORApplication *application;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    application = [[GDTCORApplication alloc] init];
  });
  return application;
}

- (instancetype)init {
  self = [super init];
  if (self) {
    // This class will be instantiated in the foreground.
    _isRunningInBackground = NO;

#if TARGET_OS_IOS || TARGET_OS_TV
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self
                           selector:@selector(iOSApplicationDidEnterBackground:)
                               name:UIApplicationDidEnterBackgroundNotification
                             object:nil];
    [notificationCenter addObserver:self
                           selector:@selector(iOSApplicationWillEnterForeground:)
                               name:UIApplicationWillEnterForegroundNotification
                             object:nil];

    NSString *name = UIApplicationWillTerminateNotification;
    [notificationCenter addObserver:self
                           selector:@selector(iOSApplicationWillTerminate:)
                               name:name
                             object:nil];

#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
    if (@available(iOS 13, tvOS 13.0, *)) {
      [notificationCenter addObserver:self
                             selector:@selector(iOSApplicationWillEnterForeground:)
                                 name:UISceneWillEnterForegroundNotification
                               object:nil];
      [notificationCenter addObserver:self
                             selector:@selector(iOSApplicationDidEnterBackground:)
                                 name:UISceneWillDeactivateNotification
                               object:nil];
    }
#endif  // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000

#elif TARGET_OS_OSX
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self
                           selector:@selector(macOSApplicationWillTerminate:)
                               name:NSApplicationWillTerminateNotification
                             object:nil];

#elif TARGET_OS_WATCH
    // TODO: Notification on watchOS platform is currently posted by strings which are frangible.
    // TODO: Needs improvements here.
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self
                           selector:@selector(iOSApplicationDidEnterBackground:)
                               name:@"UIApplicationDidEnterBackgroundNotification"
                             object:nil];
    [notificationCenter addObserver:self
                           selector:@selector(iOSApplicationWillEnterForeground:)
                               name:@"UIApplicationWillEnterForegroundNotification"
                             object:nil];

    // Adds observers for app extension on watchOS platform
    [notificationCenter addObserver:self
                           selector:@selector(iOSApplicationDidEnterBackground:)
                               name:NSExtensionHostDidEnterBackgroundNotification
                             object:nil];
    [notificationCenter addObserver:self
                           selector:@selector(iOSApplicationWillEnterForeground:)
                               name:NSExtensionHostWillEnterForegroundNotification
                             object:nil];
#endif
  }
  return self;
}

#if TARGET_OS_WATCH
/** Generates and maps a unique background identifier to the given semaphore.
 *
 * @param semaphore The semaphore to map.
 * @return A unique GDTCORBackgroundIdentifier mapped to the given semaphore.
 */
+ (GDTCORBackgroundIdentifier)createAndMapBackgroundIdentifierToSemaphore:
    (dispatch_semaphore_t)semaphore {
  __block GDTCORBackgroundIdentifier bgID = GDTCORBackgroundIdentifierInvalid;
  dispatch_queue_t queue = gSemaphoreQueue;
  NSMutableDictionary<NSNumber *, dispatch_semaphore_t> *map = gBackgroundIdentifierToSemaphoreMap;
  if (queue && map) {
    dispatch_sync(queue, ^{
      bgID = arc4random();
      NSNumber *bgIDNumber = @(bgID);
      while (bgID == GDTCORBackgroundIdentifierInvalid || map[bgIDNumber]) {
        bgID = arc4random();
        bgIDNumber = @(bgID);
      }
      map[bgIDNumber] = semaphore;
    });
  }
  return bgID;
}

/** Returns the semaphore mapped to given bgID and removes the value from the map.
 *
 * @param bgID The unique NSUInteger as GDTCORBackgroundIdentifier.
 * @return The semaphore mapped by given bgID.
 */
+ (dispatch_semaphore_t)semaphoreForBackgroundIdentifier:(GDTCORBackgroundIdentifier)bgID {
  __block dispatch_semaphore_t semaphore;
  dispatch_queue_t queue = gSemaphoreQueue;
  NSMutableDictionary<NSNumber *, dispatch_semaphore_t> *map = gBackgroundIdentifierToSemaphoreMap;
  NSNumber *bgIDNumber = @(bgID);
  if (queue && map) {
    dispatch_sync(queue, ^{
      semaphore = map[bgIDNumber];
      [map removeObjectForKey:bgIDNumber];
    });
  }
  return semaphore;
}
#endif

- (GDTCORBackgroundIdentifier)beginBackgroundTaskWithName:(NSString *)name
                                        expirationHandler:(void (^)(void))handler {
  __block GDTCORBackgroundIdentifier bgID = GDTCORBackgroundIdentifierInvalid;
#if !TARGET_OS_WATCH
  bgID = [[self sharedApplicationForBackgroundTask] beginBackgroundTaskWithName:name
                                                              expirationHandler:handler];
#if !NDEBUG
  if (bgID != GDTCORBackgroundIdentifierInvalid) {
    GDTCORLogDebug(@"Creating background task with name:%@ bgID:%ld", name, (long)bgID);
  }
#endif  // !NDEBUG
#elif TARGET_OS_WATCH
  dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
  bgID = [GDTCORApplication createAndMapBackgroundIdentifierToSemaphore:semaphore];
  if (bgID != GDTCORBackgroundIdentifierInvalid) {
    GDTCORLogDebug(@"Creating activity with name:%@ bgID:%ld on watchOS.", name, (long)bgID);
  }
  [[self sharedNSProcessInfoForBackgroundTask]
      performExpiringActivityWithReason:name
                             usingBlock:^(BOOL expired) {
                               if (expired) {
                                 if (handler) {
                                   handler();
                                 }
                                 dispatch_semaphore_signal(semaphore);
                                 GDTCORLogDebug(
                                     @"Activity with name:%@ bgID:%ld on watchOS is expiring.",
                                     name, (long)bgID);
                               } else {
                                 dispatch_semaphore_wait(
                                     semaphore,
                                     dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC));
                               }
                             }];
#endif
  return bgID;
}

- (void)endBackgroundTask:(GDTCORBackgroundIdentifier)bgID {
#if !TARGET_OS_WATCH
  if (bgID != GDTCORBackgroundIdentifierInvalid) {
    GDTCORLogDebug(@"Ending background task with ID:%ld was successful", (long)bgID);
    [[self sharedApplicationForBackgroundTask] endBackgroundTask:bgID];
    return;
  }
#elif TARGET_OS_WATCH
  if (bgID != GDTCORBackgroundIdentifierInvalid) {
    dispatch_semaphore_t semaphore = [GDTCORApplication semaphoreForBackgroundIdentifier:bgID];
    GDTCORLogDebug(@"Ending activity with bgID:%ld on watchOS.", (long)bgID);
    if (semaphore) {
      dispatch_semaphore_signal(semaphore);
      GDTCORLogDebug(@"Signaling semaphore with bgID:%ld on watchOS.", (long)bgID);
    } else {
      GDTCORLogDebug(@"Semaphore with bgID:%ld is nil on watchOS.", (long)bgID);
    }
  }
#endif  // !TARGET_OS_WATCH
}

#pragma mark - App environment helpers

- (BOOL)isAppExtension {
  BOOL appExtension = [[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"];
  return appExtension;
}

/** Returns a UIApplication or NSProcessInfo instance if on the appropriate platform.
 *
 * @return The shared UIApplication or NSProcessInfo if on the appropriate platform.
 */
#if TARGET_OS_IOS || TARGET_OS_TV
- (nullable UIApplication *)sharedApplicationForBackgroundTask {
#elif TARGET_OS_WATCH
- (nullable NSProcessInfo *)sharedNSProcessInfoForBackgroundTask {
#else
- (nullable id)sharedApplicationForBackgroundTask {
#endif
  id sharedInstance = nil;
#if TARGET_OS_IOS || TARGET_OS_TV
  if (![self isAppExtension]) {
    Class uiApplicationClass = NSClassFromString(@"UIApplication");
    if (uiApplicationClass &&
        [uiApplicationClass respondsToSelector:(NSSelectorFromString(@"sharedApplication"))]) {
      sharedInstance = [uiApplicationClass sharedApplication];
    }
  }
#elif TARGET_OS_WATCH
  sharedInstance = [NSProcessInfo processInfo];
#endif
  return sharedInstance;
}

#pragma mark - UIApplicationDelegate and WKExtensionDelegate

#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH
- (void)iOSApplicationDidEnterBackground:(NSNotification *)notif {
  _isRunningInBackground = YES;

  NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter];
  GDTCORLogDebug(@"%@", @"GDTCORPlatform is sending a notif that the app is backgrounding.");
  [notifCenter postNotificationName:kGDTCORApplicationDidEnterBackgroundNotification object:nil];
}

- (void)iOSApplicationWillEnterForeground:(NSNotification *)notif {
  _isRunningInBackground = NO;

  NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter];
  GDTCORLogDebug(@"%@", @"GDTCORPlatform is sending a notif that the app is foregrounding.");
  [notifCenter postNotificationName:kGDTCORApplicationWillEnterForegroundNotification object:nil];
}
#endif  // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH

#pragma mark - UIApplicationDelegate

#if TARGET_OS_IOS || TARGET_OS_TV
- (void)iOSApplicationWillTerminate:(NSNotification *)notif {
  NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter];
  GDTCORLogDebug(@"%@", @"GDTCORPlatform is sending a notif that the app is terminating.");
  [notifCenter postNotificationName:kGDTCORApplicationWillTerminateNotification object:nil];
}
#endif  // TARGET_OS_IOS || TARGET_OS_TV

#pragma mark - NSApplicationDelegate

#if TARGET_OS_OSX
- (void)macOSApplicationWillTerminate:(NSNotification *)notif {
  NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter];
  GDTCORLogDebug(@"%@", @"GDTCORPlatform is sending a notif that the app is terminating.");
  [notifCenter postNotificationName:kGDTCORApplicationWillTerminateNotification object:nil];
}
#endif  // TARGET_OS_OSX

@end
