// Copyright 2018 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 "Private/GULSwizzler.h"

#import <objc/runtime.h>

#import <GoogleUtilities/GULLogger.h>
#import "../Common/GULLoggerCodes.h"

#ifdef GUL_UNSWIZZLING_ENABLED
#import <GoogleUtilities/GULSwizzlingCache.h>
// We need a private method for an assert.
#import <GoogleUtilities/GULSwizzlingCache_Private.h>
#endif

static GULLoggerService kGULLoggerSwizzler = @"[GoogleUtilites/MethodSwizzler]";

dispatch_queue_t GetGULSwizzlingQueue() {
  static dispatch_queue_t queue;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    queue = dispatch_queue_create("com.google.GULSwizzler", DISPATCH_QUEUE_SERIAL);
  });
  return queue;
}

@implementation GULSwizzler

+ (void)swizzleClass:(Class)aClass
            selector:(SEL)selector
     isClassSelector:(BOOL)isClassSelector
           withBlock:(nullable id)block {
  dispatch_sync(GetGULSwizzlingQueue(), ^{
    NSAssert(selector, @"The selector cannot be NULL");
    NSAssert(aClass, @"The class cannot be Nil");
    Class resolvedClass = aClass;
    Method method = nil;
    if (isClassSelector) {
      method = class_getClassMethod(aClass, selector);
      resolvedClass = object_getClass(aClass);
    } else {
      method = class_getInstanceMethod(aClass, selector);
    }
    NSAssert(method, @"You're attempting to swizzle a method that doesn't exist. (%@, %@)",
             NSStringFromClass(resolvedClass), NSStringFromSelector(selector));
    IMP newImp = imp_implementationWithBlock(block);

#ifdef GUL_UNSWIZZLING_ENABLED
    IMP currentImp = class_getMethodImplementation(resolvedClass, selector);
    [[GULSwizzlingCache sharedInstance] cacheCurrentIMP:currentImp
                                              forNewIMP:newImp
                                               forClass:resolvedClass
                                           withSelector:selector];
#endif

    const char *typeEncoding = method_getTypeEncoding(method);
    __unused IMP originalImpOfClass =
        class_replaceMethod(resolvedClass, selector, newImp, typeEncoding);

#ifdef GUL_UNSWIZZLING_ENABLED
    // If !originalImpOfClass, then the IMP came from a superclass.
    if (originalImpOfClass) {
      if (originalImpOfClass !=
          [[GULSwizzlingCache sharedInstance] originalIMPOfCurrentIMP:currentImp]) {
        GULLogWarning(kGULLoggerSwizzler, NO,
                      [NSString stringWithFormat:@"I-SWZ%06ld",
                                                 (long)kGULSwizzlerMessageCodeMethodSwizzling000],
                      @"Swizzling class: %@ SEL:%@ after it has been previously been swizzled.",
                      NSStringFromClass(resolvedClass), NSStringFromSelector(selector));
      }
    }
#endif
  });
}

+ (void)unswizzleClass:(Class)aClass selector:(SEL)selector isClassSelector:(BOOL)isClassSelector {
#ifdef GUL_UNSWIZZLING_ENABLED
  dispatch_sync(GetGULSwizzlingQueue(), ^{
    NSAssert(aClass != nil && selector != nil, @"You cannot unswizzle a nil class or selector.");
    Method method = nil;
    Class resolvedClass = aClass;
    if (isClassSelector) {
      resolvedClass = object_getClass(aClass);
      method = class_getClassMethod(aClass, selector);
    } else {
      method = class_getInstanceMethod(aClass, selector);
    }
    NSAssert(method, @"Couldn't find the method you're unswizzling in the runtime.");
    IMP originalImp =
        [[GULSwizzlingCache sharedInstance] cachedIMPForClass:resolvedClass withSelector:selector];
    NSAssert(originalImp, @"This class/selector combination hasn't been swizzled");
    IMP currentImp = method_setImplementation(method, originalImp);
    BOOL didRemoveBlock = imp_removeBlock(currentImp);
    NSAssert(didRemoveBlock, @"Wasn't able to remove the block of a swizzled IMP.");
    [[GULSwizzlingCache sharedInstance] clearCacheForSwizzledIMP:currentImp
                                                        selector:selector
                                                          aClass:resolvedClass];
  });
#else
  NSAssert(NO, @"Unswizzling is disabled.");
#endif
}

+ (nullable IMP)originalImplementationForClass:(Class)aClass
                                      selector:(SEL)selector
                               isClassSelector:(BOOL)isClassSelector {
#ifdef GUL_UNSWIZZLING_ENABLED
  __block IMP originalImp = nil;
  dispatch_sync(GetGULSwizzlingQueue(), ^{
    Class resolvedClass = isClassSelector ? object_getClass(aClass) : aClass;
    originalImp =
        [[GULSwizzlingCache sharedInstance] cachedIMPForClass:resolvedClass withSelector:selector];
    NSAssert(originalImp, @"The IMP for this class/selector combo doesn't exist (%@, %@).",
             NSStringFromClass(resolvedClass), NSStringFromSelector(selector));
  });
  return originalImp;
#else
  NSAssert(NO, @"Unswizzling is disabled and the original IMP is not cached.");
  return nil;
#endif
}

+ (nullable IMP)currentImplementationForClass:(Class)aClass
                                     selector:(SEL)selector
                              isClassSelector:(BOOL)isClassSelector {
  NSAssert(selector, @"The selector cannot be NULL");
  NSAssert(aClass, @"The class cannot be Nil");
  if (selector == NULL || aClass == nil) {
    return nil;
  }
  __block IMP currentIMP = nil;
  dispatch_sync(GetGULSwizzlingQueue(), ^{
    Method method = nil;
    if (isClassSelector) {
      method = class_getClassMethod(aClass, selector);
    } else {
      method = class_getInstanceMethod(aClass, selector);
    }
    NSAssert(method, @"The Method for this class/selector combo doesn't exist (%@, %@).",
             NSStringFromClass(aClass), NSStringFromSelector(selector));
    if (method == nil) {
      return;
    }
    currentIMP = method_getImplementation(method);
    NSAssert(currentIMP, @"The IMP for this class/selector combo doesn't exist (%@, %@).",
             NSStringFromClass(aClass), NSStringFromSelector(selector));
  });
  return currentIMP;
}

+ (BOOL)selector:(SEL)selector existsInClass:(Class)aClass isClassSelector:(BOOL)isClassSelector {
  Method method = isClassSelector ? class_getClassMethod(aClass, selector)
                                  : class_getInstanceMethod(aClass, selector);
  return method != nil;
}

+ (NSArray<id> *)ivarObjectsForObject:(id)object {
  NSMutableArray *array = [NSMutableArray array];
  unsigned int count;
  Ivar *vars = class_copyIvarList([object class], &count);
  for (NSUInteger i = 0; i < count; i++) {
    const char *typeEncoding = ivar_getTypeEncoding(vars[i]);
    // Check to see if the ivar is an object.
    if (strncmp(typeEncoding, "@", 1) == 0) {
      id ivarObject = object_getIvar(object, vars[i]);
      [array addObject:ivarObject];
    }
  }
  free(vars);
  return array;
}

@end
