/*
 *  Copyright (c) 2014-present, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 *
 */

#import "CKComponentViewAttribute.h"

#import <objc/runtime.h>
#import <unordered_map>

#import <RenderCore/RCAssert.h>
#import <RenderCore/RCAssert.h>
#import <RenderCore/RCEqualityHelpers.h>
#import <RenderCore/CKMacros.h>

/**
 * Helper macro for asserting that an @encode type is the same size as
 * a primitive type.
 */
#if DEBUG
#define RCCAssertSizeOfEquals(type, encodedType, ...) do {          \
  NSUInteger encodedTypeSize = 0;                                   \
  NSGetSizeAndAlignment((encodedType), &encodedTypeSize, nullptr);  \
  RCCAssert(sizeof(type) == encodedTypeSize, ##__VA_ARGS__);        \
} while (0)
#else
#define RCCAssertSizeOfEquals(type, encodedType, ...) do {} while(0)
#endif

struct SetterCacheKey {
  Class cls;
  SEL sel;

  bool operator==(const SetterCacheKey &other) const {
    return cls == other.cls && sel_isEqual(sel, other.sel);
  };
};

namespace std {
  template<>
  struct hash<SetterCacheKey>
  {
    std::size_t operator()(const SetterCacheKey &key) const
    {
      NSUInteger subhashes[] = { [key.cls hash], std::hash<void *>()((void *)key.sel) };
      return RCIntegerArrayHash(subhashes, CK_ARRAY_COUNT(subhashes));
    }
  };
}

struct CachedSetter {
  NSInvocation *invocation;
  NSUInteger argumentSize;
  const char *argumentType;

  CachedSetter(NSInvocation *inv, NSUInteger argSize, const char *argType) : invocation(inv), argumentSize(argSize), argumentType(argType) {}
};

static const CachedSetter &cachedSetterInvocation(id object, SEL setter)
{
  RCCAssertMainThread();
  static auto *cachedInvocations = new std::unordered_map<SetterCacheKey, CachedSetter>();
  SetterCacheKey key = {[object class], setter};
  auto existingInvocation = cachedInvocations->find(key);
  if (existingInvocation != cachedInvocations->end()) {
    return existingInvocation->second;
  }

  NSMethodSignature *sig = [object methodSignatureForSelector:setter];
  // If the setter actually takes an NSValue id as the argument, we shouldn't unbox to the primitive type.
  const char *argumentType = [sig getArgumentTypeAtIndex:2];
  if (strcmp(argumentType, @encode(id)) != 0) {
    NSUInteger valueSize;
    NSGetSizeAndAlignment(argumentType, &valueSize, NULL);
    return cachedInvocations->emplace(key, CachedSetter([NSInvocation invocationWithMethodSignature:sig], valueSize, argumentType)).first->second;
  } else {
    return cachedInvocations->emplace(key, CachedSetter(nil, 0, nullptr)).first->second;
  }
}

static void performSetter(id object, SEL setter, id value)
{
  if ([value isKindOfClass:[NSValue class]]) {
    const CachedSetter &set = cachedSetterInvocation(object, setter);
    if (set.invocation) {
      if ([value isKindOfClass:[NSNumber class]]) {
        // We special case NSNumber because getting the correct byte width on both sides
        // is either hard (e.g. NSInteger), or impossible (e.g. CGFloat) on all architectures
        // simultaneously.
        // See https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
        // for more information on type encodings
        if (set.argumentType == nullptr || strlen(set.argumentType) != 1) {
          RCCAssert(NO, @"NSNumber: %@ cannot be used as an argument to a selector requiring '%s'; selector: %@; class %@",
                    value, set.argumentType ?: "NULL", NSStringFromSelector(setter), [object class]);
          return;
        }
        NSNumber *numValue = (NSNumber *)value;
        switch (*set.argumentType) {
          case 'c': {
            RCCAssertSizeOfEquals(char, set.argumentType, @"");
            char charValue = [numValue charValue];
            [set.invocation setArgument:&charValue atIndex:2];
            break;
          }
          case 'i': {
            RCCAssertSizeOfEquals(int, set.argumentType, @"");
            int intValue = [numValue intValue];
            [set.invocation setArgument:&intValue atIndex:2];
            break;
          }
          case 's': {
            RCCAssertSizeOfEquals(short, set.argumentType, @"");
            short shortValue = [numValue shortValue];
            [set.invocation setArgument:&shortValue atIndex:2];
            break;
          }
          case 'l': {
            // This is inconsistent, from the docs: "l is treated as a 32-bit quantity on 64-bit programs."
            RCCAssertSizeOfEquals(int32_t, set.argumentType, @"");
            int32_t longValue = [numValue intValue];
            [set.invocation setArgument:&longValue atIndex:2];
            break;
          }
          case 'q': {
            RCCAssertSizeOfEquals(long long, set.argumentType, @"");
            long long longLongValue = [numValue longLongValue];
            [set.invocation setArgument:&longLongValue atIndex:2];
            break;
          }
          case 'C': {
            RCCAssertSizeOfEquals(unsigned char, set.argumentType, @"");
            unsigned char uCharValue = [numValue unsignedCharValue];
            [set.invocation setArgument:&uCharValue atIndex:2];
            break;
          }
          case 'I': {
            RCCAssertSizeOfEquals(unsigned int, set.argumentType, @"");
            unsigned int uIntValue = [numValue unsignedIntValue];
            [set.invocation setArgument:&uIntValue atIndex:2];
            break;
          }
          case 'S': {
            RCCAssertSizeOfEquals(unsigned short, set.argumentType, @"");
            unsigned short uShortValue = [numValue unsignedShortValue];
            [set.invocation setArgument:&uShortValue atIndex:2];
            break;
          }
          case 'L': {
            // This is also inconsistent, and undocumented
            RCCAssertSizeOfEquals(uint32_t, set.argumentType, @"");
            uint32_t uLongValue = [numValue unsignedIntValue];
            [set.invocation setArgument:&uLongValue atIndex:2];
            break;
          }
          case 'Q': {
            RCCAssertSizeOfEquals(unsigned long long, set.argumentType, @"");
            unsigned long long uLongLongValue = [numValue unsignedLongLongValue];
            [set.invocation setArgument:&uLongLongValue atIndex:2];
            break;
          }
          case 'f': {
            RCCAssertSizeOfEquals(float, set.argumentType, @"");
            float floatValue = [numValue floatValue];
            [set.invocation setArgument:&floatValue atIndex:2];
            break;
          }
          case 'd': {
            RCCAssertSizeOfEquals(double, set.argumentType, @"");
            double doubleValue = [numValue doubleValue];
            [set.invocation setArgument:&doubleValue atIndex:2];
            break;
          }
          case 'B': {
            RCCAssertSizeOfEquals(BOOL, set.argumentType, @"");
            BOOL boolValue = [numValue boolValue];
            [set.invocation setArgument:&boolValue atIndex:2];
            break;
          }
          default:
            // This should just be: 'v', '*', '@', '#', ':', '?', none of which should be boxed as NSNumber
            RCCAssert(NO, @"NSNumber: %@ cannot be used as an argument to a selector requiring '%s'", value, set.argumentType);
            return;
        }
      } else {
        char buf[set.argumentSize];
        [value getValue:buf];
        [set.invocation setArgument:buf atIndex:2];
      }
      [set.invocation setSelector:setter];
      [set.invocation invokeWithTarget:object];
      return;
    }
  }

  // ARC is worried that the selector might have a return value it doesn't know about, or be annotated with ns_consumed.
  // Neither is typically the case for setters, so ignore the warning.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  [object performSelector:setter withObject:value];
#pragma clang diagnostic pop
}

CKComponentViewAttribute::CKComponentViewAttribute(const std::string &ident,
                           void (^app)(id view, id value),
                           void (^unapp)(id view, id value),
                           void (^upd)(id view, id oldValue, id newValue)) :
  identifier(ident),
  applicator(app),
  unapplicator(unapp),
  updater(upd) {};

CKComponentViewAttribute::CKComponentViewAttribute(SEL setter) noexcept :
identifier(sel_getName(setter)),
applicator(^(UIView *view, id value){
  performSetter(view, setter, value);
}) {}

// Explicit destructor to prevent inlining, reduce code size. See D1814602.
CKComponentViewAttribute::~CKComponentViewAttribute() {}

CKComponentViewAttribute CKComponentViewAttribute::LayerAttribute(SEL setter) noexcept
{
  return CKComponentViewAttribute(std::string("layer") + sel_getName(setter), ^(UIView *view, id value){
    performSetter(view.layer, setter, value);
  });
}

template class std::unordered_map<CKComponentViewAttribute, CKBoxedValue>;
