/* Copyright (c) 2011 Google Inc.
 *
 * 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.
 */

#include <objc/runtime.h>

//#import <GoogleAPIClientForREST/GTLRObject.h>
//#import <GoogleAPIClientForREST/GTLRRuntimeCommon.h>
//#import <GoogleAPIClientForREST/GTLRUtilities.h>

#import "GTLRObject.h"
#import "GTLRRuntimeCommon.h"
#import "GTLRUtilities.h"

#import "GTLRDefines.h"

static NSString *const kUserDataPropertyKey = @"_userData";

static NSString *const kGTLRObjectJSONCoderKey = @"json";

static NSMutableDictionary *DeepMutableCopyOfJSONDictionary(NSDictionary *initialJSON);

@interface GTLRObject () <GTLRRuntimeCommon>

@property(nonatomic, strong) id<GTLRObjectClassResolver>objectClassResolver;

@end

@implementation GTLRObject {
  // Any complex object hung off this object goes into the cache so the
  // next fetch will get the same object back instead of having to recreate
  // it.
  NSMutableDictionary *_childCache;
}

@synthesize JSON = _json,
            objectClassResolver = _objectClassResolver,
            userProperties = _userProperties;

+ (instancetype)object {
  return [[self alloc] init];
}

+ (instancetype)objectWithJSON:(NSDictionary *)dict {
  GTLRObject *obj = [self object];
  obj->_json = DeepMutableCopyOfJSONDictionary(dict);
  return obj;
}

+ (instancetype)objectWithJSON:(nullable NSDictionary *)dict
           objectClassResolver:(id<GTLRObjectClassResolver>)objectClassResolver {
  GTLRObject *obj = [self objectWithJSON:dict];
  obj->_objectClassResolver = objectClassResolver;
  return obj;
}

+ (NSDictionary<NSString *, NSString *> *)propertyToJSONKeyMap {
  return nil;
}

+ (NSDictionary<NSString *, Class> *)arrayPropertyToClassMap {
  return nil;
}

+ (Class)classForAdditionalProperties {
  return Nil;
}

+ (BOOL)isKindValidForClassRegistry {
  return YES;
}

- (BOOL)isEqual:(GTLRObject *)other {
  if (self == other) return YES;
  if (other == nil) return NO;

  // The objects should be the same class, or one should be a subclass of the
  // other's class
  if (![other isKindOfClass:[self class]]
      && ![self isKindOfClass:[other class]]) return NO;

  // What we're not comparing here:
  //   properties
  return GTLR_AreEqualOrBothNil(_json, [other JSON]);
}

// By definition, for two objects to potentially be considered equal,
// they must have the same hash value.  The hash is mostly ignored,
// but removeObjectsInArray: in Leopard does seem to check the hash,
// and NSObject's default hash method just returns the instance pointer.
// We'll define hash here for all of our GTLRObjects.
- (NSUInteger)hash {
  return (NSUInteger) (__bridge void *) [GTLRObject class];
}

- (id)copyWithZone:(NSZone *)zone {
  GTLRObject *newObject = [[[self class] allocWithZone:zone] init];
  newObject.JSON = DeepMutableCopyOfJSONDictionary(self.JSON);
  newObject.objectClassResolver = self.objectClassResolver;

  // What we're not copying:
  //   userProperties
  return newObject;
}

- (NSString *)descriptionWithLocale:(id)locale {
  return self.description;
}

+ (BOOL)supportsSecureCoding {
  return YES;
}

- (instancetype)initWithCoder:(NSCoder *)decoder {
  self = [super init];
  if (self) {
    // NSDictionary/NSArray seem to allow strings and numbers with secure coding
    // just fine, but to allow sub arrays or dictionaries (or an null) the
    // classes have to be explicitly listed to decode correctly.
    NSSet *expectedClasses =
        [NSSet setWithObjects:
           [NSMutableDictionary class], [NSMutableArray class], [NSNull class], nil];
    _json = [decoder decodeObjectOfClasses:expectedClasses
                                    forKey:kGTLRObjectJSONCoderKey];
  }
  return self;
}

- (void)encodeWithCoder:(NSCoder *)encoder {
  [encoder encodeObject:_json forKey:kGTLRObjectJSONCoderKey];
}

#pragma mark JSON values

- (void)setJSONValue:(id)obj forKey:(NSString *)key {
  NSMutableDictionary *dict = self.JSON;
  if (dict == nil && obj != nil) {
    dict = [NSMutableDictionary dictionaryWithCapacity:1];
    self.JSON = dict;
  }
  [dict setValue:obj forKey:key];
}

- (id)JSONValueForKey:(NSString *)key {
  id obj = [self.JSON objectForKey:key];
  return obj;
}

- (NSString *)JSONString {
  NSError *error;
  NSDictionary *json = self.JSON;
  if (json) {
    NSData *data = [NSJSONSerialization dataWithJSONObject:json
                                                   options:NSJSONWritingPrettyPrinted
                                                     error:&error];
    GTLR_DEBUG_ASSERT(data != nil, @"JSONString generate failed: %@\n JSON: %@", error, json);
    if (data) {
      NSString *jsonStr = [[NSString alloc] initWithData:data
                                                encoding:NSUTF8StringEncoding];
      if (jsonStr) return jsonStr;
    }
  }
  return @"";
}

- (NSArray<NSString *> *)additionalJSONKeys {
  NSArray *knownKeys = [[self class] allKnownKeys];
  NSMutableArray *result;
  NSArray *allKeys = _json.allKeys;
  if (allKeys) {
    result = [NSMutableArray arrayWithArray:allKeys];
    [result removeObjectsInArray:knownKeys];
    // Return nil instead of an empty array.
    if (result.count == 0) {
      result = nil;
    }
  }
  return result;
}

#pragma mark Partial - Fields

- (NSString *)fieldsDescription {
  NSString *str = [GTLRObject fieldsDescriptionForJSON:self.JSON];
  return str;
}

+ (NSString *)fieldsDescriptionForJSON:(NSDictionary *)targetJSON {
  // Internal routine: recursively generate a string field description
  // by joining elements
  NSArray *array = [self fieldsElementsForJSON:targetJSON];
  NSString *str = [array componentsJoinedByString:@","];
  return str;
}

+ (NSArray *)fieldsElementsForJSON:(NSDictionary *)targetJSON {
  // Internal routine: recursively generate an array of field description
  // element strings
  NSMutableArray *resultFields = [NSMutableArray array];

  // Sorting the dictionary keys gives us deterministic results when iterating
  NSArray *sortedKeys = [targetJSON.allKeys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
  for (NSString *key in sortedKeys) {
    // We'll build a comma-separated list of fields
    id value = [targetJSON objectForKey:key];
    if ([value isKindOfClass:[NSString class]]
        || [value isKindOfClass:[NSNumber class]]) {
      // Basic type (string, number), so the key is what we want
      [resultFields addObject:key];
    } else if ([value isKindOfClass:[NSDictionary class]]) {
      // Object (dictionary): "parent/child1,parent/child2,parent/child3"
      NSArray *subElements = [self fieldsElementsForJSON:value];
      for (NSString *subElem in subElements) {
        NSString *prepended = [NSString stringWithFormat:@"%@/%@",
                               key, subElem];
        [resultFields addObject:prepended];
      }
    } else if ([value isKindOfClass:[NSArray class]]) {
      // Array; we'll generate from the first array entry:
      // "parent(child1,child2,child3)"
      //
      // Open question: should this instead create the union of elements for
      // all items in the array, rather than just get fields from the first
      // array object?
      if (((NSArray *)value).count > 0) {
        id firstObj = [value objectAtIndex:0];
        if ([firstObj isKindOfClass:[NSDictionary class]]) {
          // An array of objects
          NSString *contentsStr = [self fieldsDescriptionForJSON:firstObj];
          NSString *encapsulated = [NSString stringWithFormat:@"%@(%@)",
                                    key, contentsStr];
          [resultFields addObject:encapsulated];
        } else {
          // An array of some basic type, or of arrays
          [resultFields addObject:key];
        }
      }
    } else {
      NSAssert(0, @"GTLRObject unknown field element for %@ (%@)",
               key, NSStringFromClass([value class]));
    }
  }
  return resultFields;
}

#pragma mark Partial - Patch

- (id)patchObjectFromOriginal:(GTLRObject *)original {
  GTLRObject *resultObj;
  NSMutableDictionary *resultJSON = [GTLRObject patchDictionaryForJSON:self.JSON
                                                      fromOriginalJSON:original.JSON];
  if (resultJSON.count > 0) {
    // Avoid an extra copy by assigning the JSON directly rather than using +objectWithJSON:
    resultObj = [[self class] object];
    resultObj.JSON = resultJSON;
  } else {
    // Client apps should not attempt to patch with an object containing
    // empty JSON
    resultObj = nil;
  }
  return resultObj;
}

+ (NSMutableDictionary *)patchDictionaryForJSON:(NSDictionary *)newJSON
                               fromOriginalJSON:(NSDictionary *)originalJSON {
  // Internal recursive routine to create an object suitable for
  // our patch semantics
  NSMutableDictionary *resultJSON = [NSMutableDictionary dictionary];

  // Iterate through keys present in the old object
  NSArray *originalKeys = originalJSON.allKeys;
  for (NSString *key in originalKeys) {
    id originalValue = [originalJSON objectForKey:key];
    id newValue = [newJSON valueForKey:key];
    if (newValue == nil) {
      // There is no new value for this key, so set the value to NSNull
      [resultJSON setValue:[NSNull null] forKey:key];
    } else if (!GTLR_AreEqualOrBothNil(originalValue, newValue)) {
      // The values for this key differ
      if ([originalValue isKindOfClass:[NSDictionary class]]
          && [newValue isKindOfClass:[NSDictionary class]]) {
        // Both are objects; recurse
        NSMutableDictionary *subDict = [self patchDictionaryForJSON:newValue
                                                   fromOriginalJSON:originalValue];
        [resultJSON setValue:subDict forKey:key];
      } else {
        // They are non-object values; the new replaces the old. Per the
        // documentation for patch, this replaces entire arrays.
        [resultJSON setValue:newValue forKey:key];
      }
    } else {
      // The values are the same; omit this key-value pair
    }
  }

  // Iterate through keys present only in the new object, and add them to the
  // result
  NSMutableArray *newKeys = [NSMutableArray arrayWithArray:newJSON.allKeys];
  [newKeys removeObjectsInArray:originalKeys];

  for (NSString *key in newKeys) {
    id value = [newJSON objectForKey:key];
    [resultJSON setValue:value forKey:key];
  }
  return resultJSON;
}

+ (id)nullValue {
  return [NSNull null];
}

#pragma mark Additional Properties

- (id)additionalPropertyForName:(NSString *)name {
  // Return the cached object, if any, before creating one.
  id result = [self cacheChildForKey:name];
  if (result != nil) {
    return result;
  }

  Class defaultClass = [[self class] classForAdditionalProperties];
  id jsonObj = [self JSONValueForKey:name];
  BOOL shouldCache = NO;
  if (jsonObj != nil) {
    id<GTLRObjectClassResolver>objectClassResolver = self.objectClassResolver;
    result = [GTLRRuntimeCommon objectFromJSON:jsonObj
                                  defaultClass:defaultClass
                           objectClassResolver:objectClassResolver
                                   isCacheable:&shouldCache];
  }

  [self setCacheChild:(shouldCache ? result : nil)
               forKey:name];
  return result;
}

- (void)setAdditionalProperty:(id)obj forName:(NSString *)name {
  BOOL shouldCache = NO;
  Class defaultClass = [[self class] classForAdditionalProperties];
  id json = [GTLRRuntimeCommon jsonFromAPIObject:obj
                                   expectedClass:defaultClass
                                     isCacheable:&shouldCache];
  [self setJSONValue:json forKey:name];
  [self setCacheChild:(shouldCache ? obj : nil)
               forKey:name];
}

- (NSDictionary<NSString *, id> *)additionalProperties {
  NSMutableDictionary *result = [NSMutableDictionary dictionary];

  NSArray *propertyNames = [self additionalJSONKeys];
  for (NSString *name in propertyNames) {
    id obj = [self additionalPropertyForName:name];
    [result setObject:obj forKey:name];
  }

  return result;
}

#pragma mark Child Cache methods

// There is no property for _childCache as there shouldn't be KVC/KVO
// support for it, it's an implementation detail.

- (void)setCacheChild:(id)obj forKey:(NSString *)key {
  if (_childCache == nil && obj != nil) {
    _childCache = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
                   obj, key, nil];
  } else {
    [_childCache setValue:obj forKey:key];
  }
}

- (id)cacheChildForKey:(NSString *)key {
  id obj = [_childCache objectForKey:key];
  return obj;
}

#pragma mark Support methods

+ (NSMutableArray *)allDeclaredProperties {
  NSMutableArray *array = [NSMutableArray array];

  // walk from this class up the hierarchy to GTLRObject
  Class topClass = class_getSuperclass([GTLRObject class]);
  for (Class currClass = self;
       currClass != topClass;
       currClass = class_getSuperclass(currClass)) {
    // step through this class's properties, and add the property names to the
    // array
    objc_property_t *properties = class_copyPropertyList(currClass, NULL);
    if (properties) {
      for (objc_property_t *prop = properties;
           *prop != NULL;
           ++prop) {
        const char *propName = property_getName(*prop);
        // We only want dynamic properties; their attributes contain ",D".
        const char *attr = property_getAttributes(*prop);
        const char *dynamicMarker = strstr(attr, ",D");
        if (dynamicMarker &&
            (dynamicMarker[2] == 0 || dynamicMarker[2] == ',' )) {
          [array addObject:(id _Nonnull)@(propName)];
        }
      }
      free(properties);
    }
  }
  return array;
}

+ (NSArray *)allKnownKeys {
  NSArray *allProps = [self allDeclaredProperties];
  NSMutableArray *knownKeys = [NSMutableArray arrayWithArray:allProps];

  NSDictionary *propMap = [GTLRObject propertyToJSONKeyMapForClass:[self class]];

  NSUInteger idx = 0;
  for (NSString *propName in allProps) {
    NSString *jsonKey = [propMap objectForKey:propName];
    if (jsonKey) {
      [knownKeys replaceObjectAtIndex:idx
                           withObject:jsonKey];
    }
    ++idx;
  }
  return knownKeys;
}

- (NSString *)description {
  NSString *jsonDesc = [self JSONDescription];

  NSString *str = [NSString stringWithFormat:@"%@ %p: %@",
                   [self class], self, jsonDesc];
  return str;
}

// Internal utility for creating an appropriate description summary for the object's JSON.
- (NSString *)JSONDescription {
  // Find the list of declared and otherwise known JSON keys for this class.
  NSArray *knownKeys = [[self class] allKnownKeys];

  NSMutableString *descStr = [NSMutableString stringWithString:@"{"];

  NSString *spacer = @"";
  for (NSString *key in [[_json allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]) {
    NSString *value = nil;
    // show question mark for JSON keys not supported by a declared property:
    //   foo?:"Hi mom."
    NSString *qmark = [knownKeys containsObject:key] ? @"" : @"?";

    // determine property value to dislay
    id rawValue = [_json valueForKey:key];
    if ([rawValue isKindOfClass:[NSDictionary class]]) {
      // for dictionaries, show the list of keys:
      //   {key1,key2,key3}
      NSArray *subKeys = [((NSDictionary *)rawValue).allKeys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
      NSString *subkeyList = [subKeys componentsJoinedByString:@","];
      value = [NSString stringWithFormat:@"{%@}", subkeyList];
    } else if ([rawValue isKindOfClass:[NSArray class]]) {
      // for arrays, show the number of items in the array:
      //   [3]
      value = [NSString stringWithFormat:@"[%lu]",
               (unsigned long)((NSArray *)rawValue).count];
    } else if ([rawValue isKindOfClass:[NSString class]]) {
      // for strings, show the string in quotes:
      //   "Hi mom."
      value = [NSString stringWithFormat:@"\"%@\"", rawValue];
    } else {
      // for numbers, show just the number
      value = [rawValue description];
    }
    [descStr appendFormat:@"%@%@%@:%@", spacer, key, qmark, value];
    spacer = @" ";
  }
  [descStr appendString:@"}"];
  return descStr;
}

#pragma mark Object Instantiation

+ (GTLRObject *)objectForJSON:(NSMutableDictionary *)json
                 defaultClass:(Class)defaultClass
          objectClassResolver:(id<GTLRObjectClassResolver>)objectClassResolver {
  if (((id)json == [NSNull null]) || json.count == 0) {
    if (json != nil && defaultClass != Nil) {
      // The JSON included an empty dictionary, just create the object.
      Class classToCreate =
        GTLRObjectResolveClass(objectClassResolver,
                               [NSDictionary dictionary],
                               defaultClass);
      return [classToCreate object];
    }
    // No actual result, such as the response from a delete.
    return nil;
  }

  if (defaultClass == Nil) {
    defaultClass = self;
  }

  Class classToCreate =
    GTLRObjectResolveClass(objectClassResolver, json, defaultClass);

  // now instantiate the GTLRObject
  GTLRObject *parsedObject = [classToCreate object];
  parsedObject.objectClassResolver = objectClassResolver;
  parsedObject.JSON = json;
  return parsedObject;
}

#pragma mark Runtime Utilities

static NSMutableDictionary *gJSONKeyMapCache = nil;
static NSMutableDictionary *gArrayPropertyToClassMapCache = nil;

+ (void)initialize {
  // Note that initialize is guaranteed by the runtime to be called in a
  // thread-safe manner
  if (gJSONKeyMapCache == nil) {
    gJSONKeyMapCache = [[NSMutableDictionary alloc] init];
  }
  if (gArrayPropertyToClassMapCache == nil) {
    gArrayPropertyToClassMapCache = [[NSMutableDictionary alloc] init];
  }
}

+ (NSDictionary *)propertyToJSONKeyMapForClass:(Class<GTLRRuntimeCommon>)aClass {
  NSDictionary *resultMap =
    [GTLRRuntimeCommon mergedClassDictionaryForSelector:@selector(propertyToJSONKeyMap)
                                             startClass:aClass
                                          ancestorClass:[GTLRObject class]
                                                  cache:gJSONKeyMapCache];
  return resultMap;
}

+ (NSDictionary *)arrayPropertyToClassMapForClass:(Class<GTLRRuntimeCommon>)aClass {
  NSDictionary *resultMap =
    [GTLRRuntimeCommon mergedClassDictionaryForSelector:@selector(arrayPropertyToClassMap)
                                             startClass:aClass
                                          ancestorClass:[GTLRObject class]
                                                  cache:gArrayPropertyToClassMapCache];
  return resultMap;
}

#pragma mark Runtime Support

+ (Class<GTLRRuntimeCommon>)ancestorClass {
  return [GTLRObject class];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
  BOOL resolved = [GTLRRuntimeCommon resolveInstanceMethod:sel onClass:self];
  if (resolved)
    return YES;

  return [super resolveInstanceMethod:sel];
}

@end

@implementation GTLRCollectionObject

+ (NSString *)collectionItemsKey {
  // GTLRCollectionObject fast enumeration, indexed access, and automatic pagination
  // (when shouldFetchNextPages is enabled) applies to the object array property "items".
  // The array property's key may be different if subclasses override this method.
  return @"items";
}

- (id)objectAtIndexedSubscript:(NSUInteger)idx {
  NSString *key = [[self class] collectionItemsKey];
  NSArray *items = [self valueForKey:key];
  if (items == nil) {
    [NSException raise:NSRangeException
                format:@"index %lu beyond bounds (%@ property \"%@\" is nil)",
                       (unsigned long)idx, [self class], key];
  }
  id result = [items objectAtIndexedSubscript:idx];
  return result;
}

// NSFastEnumeration protocol
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
                                  objects:(__unsafe_unretained id _Nonnull *)stackbuf
                                    count:(NSUInteger)len {
  NSString *key = [[self class] collectionItemsKey];
  NSArray *items = [self valueForKey:key];
  NSUInteger result = [items countByEnumeratingWithState:state
                                                 objects:stackbuf
                                                   count:len];
  return result;
}

@end

@implementation GTLRDataObject

@synthesize data = _data,
            contentType = _contentType;

- (NSString *)description {
  NSString *jsonDesc = @"";
  if (self.JSON.count > 0) {
    jsonDesc = [self JSONDescription];
  }
  return [NSString stringWithFormat:@"%@ %p: %lu bytes, contentType:%@ %@",
          [self class], self, (unsigned long)self.data.length, self.contentType,
          jsonDesc];
}

- (id)copyWithZone:(NSZone *)zone {
  GTLRDataObject *newObj = [super copyWithZone:zone];
  newObj.data = [self.data copy];
  newObj.contentType = self.contentType;
  return newObj;
}

@end

@implementation GTLRResultArray

- (NSArray *)itemsWithItemClass:(Class)itemClass {
  // Return the cached array before creating on demand.
  NSString *cacheKey = @"result_array_items";
  NSMutableArray *cachedArray = [self cacheChildForKey:cacheKey];
  if (cachedArray != nil) {
    return cachedArray;
  }
  NSArray *result = nil;
  NSArray *array = (NSArray *)self.JSON;
  if (array != nil) {
    if ([array isKindOfClass:[NSArray class]]) {
      id<GTLRObjectClassResolver>objectClassResolver = self.objectClassResolver;
      result = [GTLRRuntimeCommon objectFromJSON:array
                                    defaultClass:itemClass
                             objectClassResolver:objectClassResolver
                                     isCacheable:NULL];
    } else {
#if DEBUG
      if (![array isKindOfClass:[NSNull class]]) {
        GTLR_DEBUG_LOG(@"GTLRObject: unexpected JSON: %@ should be an array, actually is a %@:\n%@",
                       NSStringFromClass([self class]),
                       NSStringFromClass([array class]),
                       array);
      }
#endif
      result = array;
    }
  }

  [self setCacheChild:result forKey:cacheKey];
  return result;
}

- (NSString *)JSONDescription {
  // Just like GTLRObject's handing of arrays, just return the count.
  return [NSString stringWithFormat:@"[%lu]", (unsigned long)self.JSON.count];
}

@end

Class GTLRObjectResolveClass(
    id<GTLRObjectClassResolver>objectClassResolver,
    NSDictionary *json,
    Class defaultClass) {
  Class result = [objectClassResolver classForJSON:json
                                      defaultClass:defaultClass];
  if (result == Nil) {
    result = defaultClass;
  }
  return result;
}

@implementation GTLRObjectClassResolver {
  NSDictionary<NSString *, Class> *_kindToClassMap;
  NSDictionary<Class, Class> *_surrogates;
}

+ (instancetype)resolverWithKindMap:(NSDictionary<NSString *, Class> *)kindStringToClassMap {
  GTLRObjectClassResolver *result = [[self alloc] initWithKindMap:kindStringToClassMap
                                                       surrogates:nil];
  return result;
}

+ (instancetype)resolverWithKindMap:(NSDictionary<NSString *, Class> *)kindStringToClassMap
                         surrogates:(NSDictionary<Class, Class> *)surrogates {
  GTLRObjectClassResolver *result = [[self alloc] initWithKindMap:kindStringToClassMap
                                                       surrogates:surrogates];
  return result;
}

- (instancetype)initWithKindMap:(NSDictionary<NSString *, Class> *)kindStringToClassMap
                     surrogates:(NSDictionary<Class, Class> *)surrogates {
  self = [super init];
  if (self) {
    _kindToClassMap = [kindStringToClassMap copy];
    _surrogates = [surrogates copy];
  }
  return self;
}

- (Class)classForJSON:(NSDictionary *)json
         defaultClass:(Class)defaultClass {
  Class result = defaultClass;

  // Apply kind map.
  BOOL shouldUseKind = (result == Nil) || [result isKindValidForClassRegistry];
  if (shouldUseKind && [json isKindOfClass:[NSDictionary class]]) {
    NSString *kind = [json valueForKey:@"kind"];
    if ([kind isKindOfClass:[NSString class]] && kind.length > 0) {
      Class dynamicClass = [_kindToClassMap objectForKey:kind];
      if (dynamicClass) {
        result = dynamicClass;
      }
    }
  }

  // Apply surrogate map.
  Class surrogate = [_surrogates objectForKey:result];
  if (surrogate) {
    result = surrogate;
  }

  return result;
}

@end

static NSMutableDictionary *DeepMutableCopyOfJSONDictionary(NSDictionary *initialJSON) {
  if (!initialJSON) return nil;

  NSMutableDictionary *result;
  CFPropertyListRef ref = CFPropertyListCreateDeepCopy(kCFAllocatorDefault,
                                                       (__bridge CFPropertyListRef)(initialJSON),
                                                       kCFPropertyListMutableContainers);
  if (ref) {
    result = CFBridgingRelease(ref);
  } else {
    // Failed to copy, probably due to a non-plist type such as NSNull.
    //
    // As a fallback, round-trip through NSJSONSerialization.
    NSError *serializationError;
    NSData *data = [NSJSONSerialization dataWithJSONObject:initialJSON
                                                   options:0
                                                     error:&serializationError];
    if (!data) {
      GTLR_DEBUG_ASSERT(0, @"Copy failed due to serialization: %@\nJSON: %@",
                        serializationError, initialJSON);
    } else {
      result = [NSJSONSerialization JSONObjectWithData:data
                                                options:NSJSONReadingMutableContainers
                                                  error:&serializationError];
      GTLR_DEBUG_ASSERT(result != nil, @"Copy failed due to deserialization: %@\nJSON: %@",
                        serializationError,
                        [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
    }
  }
  return result;
}