// Copyright 2019 Google LLC
//
// 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 <TargetConditionals.h>

#import "GoogleUtilities/AppDelegateSwizzler/Public/GoogleUtilities/GULSceneDelegateSwizzler.h"

#import "GoogleUtilities/AppDelegateSwizzler/Internal/GULSceneDelegateSwizzler_Private.h"
#import "GoogleUtilities/AppDelegateSwizzler/Public/GoogleUtilities/GULAppDelegateSwizzler.h"
#import "GoogleUtilities/Common/GULLoggerCodes.h"
#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULAppEnvironmentUtil.h"
#import "GoogleUtilities/Logger/Public/GoogleUtilities/GULLogger.h"
#import "GoogleUtilities/Network/Public/GoogleUtilities/GULMutableDictionary.h"

#import <objc/runtime.h>

#if UISCENE_SUPPORTED
API_AVAILABLE(ios(13.0), tvos(13.0))
typedef void (*GULOpenURLContextsIMP)(id, SEL, UIScene *, NSSet<UIOpenURLContext *> *);

API_AVAILABLE(ios(13.0), tvos(13.0))
typedef void (^GULSceneDelegateInterceptorCallback)(id<UISceneDelegate>);

// The strings below are the keys for associated objects.
static char const *const kGULRealIMPBySelectorKey = "GUL_realIMPBySelector";
static char const *const kGULRealClassKey = "GUL_realClass";
#endif  // UISCENE_SUPPORTED

static GULLoggerService kGULLoggerSwizzler = @"[GoogleUtilities/SceneDelegateSwizzler]";

// Since Firebase SDKs also use this for app delegate proxying, in order to not be a breaking change
// we disable App Delegate proxying when either of these two flags are set to NO.

/** Plist key that allows Firebase developers to disable App and Scene Delegate Proxying. */
static NSString *const kGULFirebaseSceneDelegateProxyEnabledPlistKey =
    @"FirebaseAppDelegateProxyEnabled";

/** Plist key that allows developers not using Firebase to disable App and Scene Delegate Proxying.
 */
static NSString *const kGULGoogleUtilitiesSceneDelegateProxyEnabledPlistKey =
    @"GoogleUtilitiesAppDelegateProxyEnabled";

/** The prefix of the Scene Delegate. */
static NSString *const kGULSceneDelegatePrefix = @"GUL_";

/**
 * This class is necessary to store the delegates in an NSArray without retaining them.
 * [NSValue valueWithNonRetainedObject] also provides this functionality, but does not provide a
 * zeroing pointer. This will cause EXC_BAD_ACCESS when trying to access the object after it is
 * dealloced. Instead, this container stores a weak, zeroing reference to the object, which
 * automatically is set to nil by the runtime when the object is dealloced.
 */
@interface GULSceneZeroingWeakContainer : NSObject

/** Stores a weak object. */
@property(nonatomic, weak) id object;

@end

@implementation GULSceneZeroingWeakContainer
@end

@implementation GULSceneDelegateSwizzler

#pragma mark - Public methods

+ (BOOL)isSceneDelegateProxyEnabled {
  return [GULAppDelegateSwizzler isAppDelegateProxyEnabled];
}

+ (void)proxyOriginalSceneDelegate {
#if UISCENE_SUPPORTED
  if ([GULAppEnvironmentUtil isAppExtension]) {
    return;
  }

  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    if (@available(iOS 13.0, tvOS 13.0, *)) {
      if (![GULSceneDelegateSwizzler isSceneDelegateProxyEnabled]) {
        return;
      }
      [[NSNotificationCenter defaultCenter]
          addObserver:self
             selector:@selector(handleSceneWillConnectToNotification:)
                 name:UISceneWillConnectNotification
               object:nil];
    }
  });
#endif  // UISCENE_SUPPORTED
}

#if UISCENE_SUPPORTED
+ (GULSceneDelegateInterceptorID)registerSceneDelegateInterceptor:(id<UISceneDelegate>)interceptor {
  NSAssert(interceptor, @"SceneDelegateProxy cannot add nil interceptor");
  NSAssert([interceptor conformsToProtocol:@protocol(UISceneDelegate)],
           @"SceneDelegateProxy interceptor does not conform to UIApplicationDelegate");

  if (!interceptor) {
    GULOSLogError(
        kGULLogSubsystem, kGULLoggerSwizzler, NO,
        [NSString
            stringWithFormat:@"I-SWZ%06ld", (long)kGULSwizzlerMessageCodeSceneDelegateSwizzling000],
        @"SceneDelegateProxy cannot add nil interceptor.");
    return nil;
  }
  if (![interceptor conformsToProtocol:@protocol(UISceneDelegate)]) {
    GULOSLogError(
        kGULLogSubsystem, kGULLoggerSwizzler, NO,
        [NSString
            stringWithFormat:@"I-SWZ%06ld", (long)kGULSwizzlerMessageCodeSceneDelegateSwizzling001],
        @"SceneDelegateProxy interceptor does not conform to UIApplicationDelegate");
    return nil;
  }

  // The ID should be the same given the same interceptor object.
  NSString *interceptorID =
      [NSString stringWithFormat:@"%@%p", kGULSceneDelegatePrefix, interceptor];
  if (!interceptorID.length) {
    GULOSLogError(
        kGULLogSubsystem, kGULLoggerSwizzler, NO,
        [NSString
            stringWithFormat:@"I-SWZ%06ld", (long)kGULSwizzlerMessageCodeSceneDelegateSwizzling002],
        @"SceneDelegateProxy cannot create Interceptor ID.");
    return nil;
  }
  GULSceneZeroingWeakContainer *weakObject = [[GULSceneZeroingWeakContainer alloc] init];
  weakObject.object = interceptor;
  [GULSceneDelegateSwizzler interceptors][interceptorID] = weakObject;
  return interceptorID;
}

+ (void)unregisterSceneDelegateInterceptorWithID:(GULSceneDelegateInterceptorID)interceptorID {
  NSAssert(interceptorID, @"SceneDelegateProxy cannot unregister nil interceptor ID.");
  NSAssert(((NSString *)interceptorID).length != 0,
           @"SceneDelegateProxy cannot unregister empty interceptor ID.");

  if (!interceptorID) {
    GULOSLogError(
        kGULLogSubsystem, kGULLoggerSwizzler, NO,
        [NSString
            stringWithFormat:@"I-SWZ%06ld", (long)kGULSwizzlerMessageCodeSceneDelegateSwizzling003],
        @"SceneDelegateProxy cannot unregister empty interceptor ID.");
    return;
  }

  GULSceneZeroingWeakContainer *weakContainer =
      [GULSceneDelegateSwizzler interceptors][interceptorID];
  if (!weakContainer.object) {
    GULOSLogError(
        kGULLogSubsystem, kGULLoggerSwizzler, NO,
        [NSString
            stringWithFormat:@"I-SWZ%06ld", (long)kGULSwizzlerMessageCodeSceneDelegateSwizzling004],
        @"SceneDelegateProxy cannot unregister interceptor that was not registered. "
         "Interceptor ID %@",
        interceptorID);
    return;
  }

  [[GULSceneDelegateSwizzler interceptors] removeObjectForKey:interceptorID];
}

#pragma mark - Helper methods

+ (GULMutableDictionary *)interceptors {
  static dispatch_once_t onceToken;
  static GULMutableDictionary *sInterceptors;
  dispatch_once(&onceToken, ^{
    sInterceptors = [[GULMutableDictionary alloc] init];
  });
  return sInterceptors;
}

+ (void)clearInterceptors {
  [[self interceptors] removeAllObjects];
}

+ (nullable NSValue *)originalImplementationForSelector:(SEL)selector object:(id)object {
  NSDictionary *realImplementationBySelector =
      objc_getAssociatedObject(object, &kGULRealIMPBySelectorKey);
  return realImplementationBySelector[NSStringFromSelector(selector)];
}

+ (void)proxyDestinationSelector:(SEL)destinationSelector
    implementationsFromSourceSelector:(SEL)sourceSelector
                            fromClass:(Class)sourceClass
                              toClass:(Class)destinationClass
                            realClass:(Class)realClass
     storeDestinationImplementationTo:
         (NSMutableDictionary<NSString *, NSValue *> *)destinationImplementationsBySelector {
  [self addInstanceMethodWithDestinationSelector:destinationSelector
            withImplementationFromSourceSelector:sourceSelector
                                       fromClass:sourceClass
                                         toClass:destinationClass];
  IMP sourceImplementation =
      [GULSceneDelegateSwizzler implementationOfMethodSelector:destinationSelector
                                                     fromClass:realClass];
  NSValue *sourceImplementationPointer = [NSValue valueWithPointer:sourceImplementation];

  NSString *destinationSelectorString = NSStringFromSelector(destinationSelector);
  destinationImplementationsBySelector[destinationSelectorString] = sourceImplementationPointer;
}

/** Copies a method identified by the methodSelector from one class to the other. After this method
 *  is called, performing [toClassInstance methodSelector] will be similar to calling
 *  [fromClassInstance methodSelector]. This method does nothing if toClass already has a method
 *  identified by methodSelector.
 *
 *  @param methodSelector The SEL that identifies both the method on the fromClass as well as the
 *      one on the toClass.
 *  @param fromClass The class from which a method is sourced.
 *  @param toClass The class to which the method is added. If the class already has a method with
 *      the same selector, this has no effect.
 */
+ (void)addInstanceMethodWithSelector:(SEL)methodSelector
                            fromClass:(Class)fromClass
                              toClass:(Class)toClass {
  [self addInstanceMethodWithDestinationSelector:methodSelector
            withImplementationFromSourceSelector:methodSelector
                                       fromClass:fromClass
                                         toClass:toClass];
}

/** Copies a method identified by the sourceSelector from the fromClass as a method for the
 *  destinationSelector on the toClass. After this method is called, performing
 *  [toClassInstance destinationSelector] will be similar to calling
 *  [fromClassInstance sourceSelector]. This method does nothing if toClass already has a method
 *  identified by destinationSelector.
 *
 *  @param destinationSelector The SEL that identifies the method on the toClass.
 *  @param sourceSelector The SEL that identifies the method on the fromClass.
 *  @param fromClass The class from which a method is sourced.
 *  @param toClass The class to which the method is added. If the class already has a method with
 *      the same selector, this has no effect.
 */
+ (void)addInstanceMethodWithDestinationSelector:(SEL)destinationSelector
            withImplementationFromSourceSelector:(SEL)sourceSelector
                                       fromClass:(Class)fromClass
                                         toClass:(Class)toClass {
  Method method = class_getInstanceMethod(fromClass, sourceSelector);
  IMP methodIMP = method_getImplementation(method);
  const char *types = method_getTypeEncoding(method);
  if (!class_addMethod(toClass, destinationSelector, methodIMP, types)) {
    GULOSLogWarning(
        kGULLogSubsystem, kGULLoggerSwizzler, NO,
        [NSString
            stringWithFormat:@"I-SWZ%06ld", (long)kGULSwizzlerMessageCodeSceneDelegateSwizzling009],
        @"Cannot copy method to destination selector %@ as it already exists",
        NSStringFromSelector(destinationSelector));
  }
}

/** Gets the IMP of the instance method on the class identified by the selector.
 *
 *  @param selector The selector of which the IMP is to be fetched.
 *  @param aClass The class from which the IMP is to be fetched.
 *  @return The IMP of the instance method identified by selector and aClass.
 */
+ (IMP)implementationOfMethodSelector:(SEL)selector fromClass:(Class)aClass {
  Method aMethod = class_getInstanceMethod(aClass, selector);
  return method_getImplementation(aMethod);
}

/** Enumerates through all the interceptors and if they respond to a given selector, executes a
 *  GULSceneDelegateInterceptorCallback with the interceptor.
 *
 *  @param methodSelector The SEL to check if an interceptor responds to.
 *  @param callback the GULSceneDelegateInterceptorCallback.
 */
+ (void)notifyInterceptorsWithMethodSelector:(SEL)methodSelector
                                    callback:(GULSceneDelegateInterceptorCallback)callback
    API_AVAILABLE(ios(13.0)) {
  if (!callback) {
    return;
  }

  NSDictionary *interceptors = [GULSceneDelegateSwizzler interceptors].dictionary;
  [interceptors enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
    GULSceneZeroingWeakContainer *interceptorContainer = obj;
    id interceptor = interceptorContainer.object;
    if (!interceptor) {
      GULOSLogWarning(
          kGULLogSubsystem, kGULLoggerSwizzler, NO,
          [NSString stringWithFormat:@"I-SWZ%06ld",
                                     (long)kGULSwizzlerMessageCodeSceneDelegateSwizzling010],
          @"SceneDelegateProxy cannot find interceptor with ID %@. Removing the interceptor.", key);
      [[GULSceneDelegateSwizzler interceptors] removeObjectForKey:key];
      return;
    }
    if ([interceptor respondsToSelector:methodSelector]) {
      callback(interceptor);
    }
  }];
}

+ (void)handleSceneWillConnectToNotification:(NSNotification *)notification {
  if (@available(iOS 13.0, tvOS 13.0, *)) {
    if ([notification.object isKindOfClass:[UIScene class]]) {
      UIScene *scene = (UIScene *)notification.object;
      [GULSceneDelegateSwizzler proxySceneDelegateIfNeeded:scene];
    }
  }
}

#pragma mark - [Donor Methods] UISceneDelegate URL handler

- (void)scene:(UIScene *)scene
    openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts API_AVAILABLE(ios(13.0), tvos(13.0)) {
  if (@available(iOS 13.0, tvOS 13.0, *)) {
    SEL methodSelector = @selector(scene:openURLContexts:);
    // Call the real implementation if the real Scene Delegate has any.
    NSValue *openURLContextsIMPPointer =
        [GULSceneDelegateSwizzler originalImplementationForSelector:methodSelector object:self];
    GULOpenURLContextsIMP openURLContextsIMP = [openURLContextsIMPPointer pointerValue];

    [GULSceneDelegateSwizzler
        notifyInterceptorsWithMethodSelector:methodSelector
                                    callback:^(id<UISceneDelegate> interceptor) {
                                      if ([interceptor
                                              conformsToProtocol:@protocol(UISceneDelegate)]) {
                                        id<UISceneDelegate> sceneInterceptor =
                                            (id<UISceneDelegate>)interceptor;
                                        [sceneInterceptor scene:scene openURLContexts:URLContexts];
                                      }
                                    }];

    if (openURLContextsIMP) {
      openURLContextsIMP(self, methodSelector, scene, URLContexts);
    }
  }
}

+ (void)proxySceneDelegateIfNeeded:(UIScene *)scene {
  Class realClass = [scene.delegate class];
  NSString *className = NSStringFromClass(realClass);

  // Skip proxying if failed to get the delegate class name for some reason (e.g. `delegate == nil`)
  // or the class has a prefix of kGULAppDelegatePrefix, which means it has been proxied before.
  if (className == nil || [className hasPrefix:kGULSceneDelegatePrefix]) {
    return;
  }

  NSString *classNameWithPrefix = [kGULSceneDelegatePrefix stringByAppendingString:className];
  NSString *newClassName =
      [NSString stringWithFormat:@"%@-%@", classNameWithPrefix, [NSUUID UUID].UUIDString];

  if (NSClassFromString(newClassName)) {
    GULOSLogError(
        kGULLogSubsystem, kGULLoggerSwizzler, NO,
        [NSString
            stringWithFormat:@"I-SWZ%06ld",
                             (long)
                                 kGULSwizzlerMessageCodeSceneDelegateSwizzlingInvalidSceneDelegate],
        @"Cannot create a proxy for Scene Delegate. Subclass already exists. Original Class"
        @": %@, subclass: %@",
        className, newClassName);
    return;
  }

  // Register the new class as subclass of the real one. Do not allocate more than the real class
  // size.
  Class sceneDelegateSubClass = objc_allocateClassPair(realClass, newClassName.UTF8String, 0);
  if (sceneDelegateSubClass == Nil) {
    GULOSLogError(
        kGULLogSubsystem, kGULLoggerSwizzler, NO,
        [NSString
            stringWithFormat:@"I-SWZ%06ld",
                             (long)
                                 kGULSwizzlerMessageCodeSceneDelegateSwizzlingInvalidSceneDelegate],
        @"Cannot create a proxy for Scene Delegate. Subclass already exists. Original Class"
        @": %@, subclass: Nil",
        className);
    return;
  }

  NSMutableDictionary<NSString *, NSValue *> *realImplementationsBySelector =
      [[NSMutableDictionary alloc] init];

  // For scene:openURLContexts:
  SEL openURLContextsSEL = @selector(scene:openURLContexts:);
  [self proxyDestinationSelector:openURLContextsSEL
      implementationsFromSourceSelector:openURLContextsSEL
                              fromClass:[GULSceneDelegateSwizzler class]
                                toClass:sceneDelegateSubClass
                              realClass:realClass
       storeDestinationImplementationTo:realImplementationsBySelector];

  // Store original implementations to a fake property of the original delegate.
  objc_setAssociatedObject(scene.delegate, &kGULRealIMPBySelectorKey,
                           [realImplementationsBySelector copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  objc_setAssociatedObject(scene.delegate, &kGULRealClassKey, realClass,
                           OBJC_ASSOCIATION_RETAIN_NONATOMIC);

  // The subclass size has to be exactly the same size with the original class size. The subclass
  // cannot have more ivars/properties than its superclass since it will cause an offset in memory
  // that can lead to overwriting the isa of an object in the next frame.
  if (class_getInstanceSize(realClass) != class_getInstanceSize(sceneDelegateSubClass)) {
    GULOSLogError(
        kGULLogSubsystem, kGULLoggerSwizzler, NO,
        [NSString
            stringWithFormat:@"I-SWZ%06ld",
                             (long)
                                 kGULSwizzlerMessageCodeSceneDelegateSwizzlingInvalidSceneDelegate],
        @"Cannot create subclass of Scene Delegate, because the created subclass is not the "
        @"same size. %@",
        className);
    NSAssert(NO, @"Classes must be the same size to swizzle isa");
    return;
  }

  // Make the newly created class to be the subclass of the real Scene Delegate class.
  objc_registerClassPair(sceneDelegateSubClass);
  if (object_setClass(scene.delegate, sceneDelegateSubClass)) {
    GULOSLogDebug(
        kGULLogSubsystem, kGULLoggerSwizzler, NO,
        [NSString
            stringWithFormat:@"I-SWZ%06ld",
                             (long)
                                 kGULSwizzlerMessageCodeSceneDelegateSwizzlingInvalidSceneDelegate],
        @"Successfully created Scene Delegate Proxy automatically. To disable the "
        @"proxy, set the flag %@ to NO (Boolean) in the Info.plist",
        [GULSceneDelegateSwizzler correctSceneDelegateProxyKey]);
  }
}

+ (NSString *)correctSceneDelegateProxyKey {
  return NSClassFromString(@"FIRCore") ? kGULFirebaseSceneDelegateProxyEnabledPlistKey
                                       : kGULGoogleUtilitiesSceneDelegateProxyEnabledPlistKey;
}

#endif  // UISCENE_SUPPORTED

@end
