//
//  TNLParameterCollection.m
//  TwitterNetworkLayer
//
//  Created on 10/24/14.
//  Copyright © 2020 Twitter. All rights reserved.
//

#import "TNL_Project.h"
#import "TNLParameterCollection.h"
#import "TNLURLCoding.h"

NS_ASSUME_NONNULL_BEGIN

NSString * const kParametersCodingKey = @"parameters";

typedef NSString *(^TNLParameterCollectionUpdateKeysAndValuesIterativeKeyBlock)(NSString *key, id obj);

TNL_OBJC_DIRECT_MEMBERS
@interface TNLParameterCollection ()

@property (nonatomic, readonly) NSDictionary *parameters;

- (instancetype)initWithDirectlyAssignedDictionary:(nullable NSDictionary<NSString *, id> *)dict;

@end

TNL_OBJC_DIRECT_MEMBERS
@implementation TNLParameterCollection
{
    @protected
    NSDictionary<NSString *, id> *_parameters;
}

@synthesize parameters = _parameters;

- (instancetype)init
{
    return [super init];
}

- (instancetype)initWithDirectlyAssignedDictionary:(nullable NSDictionary<NSString *, id> *)dict
{
    if (self = [self init]) {
        _parameters = dict; // don't copy!
    }
    return self;
}

- (instancetype)initWithURLEncodedString:(nullable NSString *)params
{
    return [self initWithURLEncodedString:params options:TNLURLDecodingOptionsNone];
}

- (instancetype)initWithURLEncodedString:(nullable NSString *)params
                                 options:(TNLURLDecodingOptions)options
{
    TNLMutableParameterCollection *mCollection = [[TNLMutableParameterCollection alloc] initWithURLEncodedString:params
                                                                                                         options:options];
    return [self initWithParameterCollection:mCollection];
}

- (instancetype)initWithDictionary:(nullable NSDictionary<NSString *, id> *)dictionary
{
    TNLMutableParameterCollection *mCollection = [[TNLMutableParameterCollection alloc] initWithDictionary:dictionary];
    return [self initWithParameterCollection:mCollection];
}

- (instancetype)initWithURL:(nullable NSURL *)URL
      parsingParameterTypes:(TNLParameterTypes)types
{
    return [self initWithURL:URL
       parsingParameterTypes:types
                     options:TNLURLDecodingOptionsNone];
}

- (instancetype)initWithURL:(nullable NSURL *)URL
      parsingParameterTypes:(TNLParameterTypes)types
                    options:(TNLURLDecodingOptions)options
{
    TNLMutableParameterCollection *mCollection = [[TNLMutableParameterCollection alloc] initWithURL:URL
                                                                              parsingParameterTypes:types
                                                                                            options:options];
    return [self initWithParameterCollection:mCollection];
}

- (instancetype)initWithParameterCollection:(nullable TNLParameterCollection *)otherCollection
{
    if (self = [super init]) {
        _parameters = [otherCollection.parameters copy];
    }
    return self;
}

#pragma mark NSCopying

- (id)copyWithZone:(nullable NSZone *)zone
{
    return self;
}

#pragma mark NSMutableCopying

- (id)mutableCopyWithZone:(nullable NSZone *)zone
{
    return [[TNLMutableParameterCollection alloc] initWithParameterCollection:self];
}

#pragma mark Count

- (NSUInteger)count
{
    return _parameters.count;
}

#pragma mark NSSecureCoding

+ (BOOL)supportsSecureCoding
{
    return YES;
}

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
    NSDictionary *d = [aDecoder decodeObjectOfClass:[NSDictionary class]
                                             forKey:kParametersCodingKey];
    return [self initWithDirectlyAssignedDictionary:d];
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:_parameters forKey:kParametersCodingKey];
}

#pragma mark NSFastEnumeration

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
                                  objects:(id __unsafe_unretained __nullable [__nonnull])buffer
                                    count:(NSUInteger)len
{
    return [_parameters countByEnumeratingWithState:state objects:buffer count:len];
}

#pragma mark Keyed Subscripting

- (nullable id)objectForKeyedSubscript:(NSString *)key
{
    return [self parameterValueForKey:key];
}

#pragma mark Access

- (nullable id)parameterValueForKey:(NSString *)key
{
    return _parameters[key];
}

#pragma mark Helpers

- (NSArray *)allKeys
{
    return _parameters.allKeys;
}

- (void)enumerateParameterKeysAndValuesUsingBlock:(void (^)(NSString *, id, BOOL *))block
{
    [_parameters enumerateKeysAndObjectsUsingBlock:block];
}

- (void)enumerateParameterKeysAndValuesWithOptions:(NSEnumerationOptions)opts
                                        usingBlock:(void (^)(NSString *, id, BOOL *))block
{
    [_parameters enumerateKeysAndObjectsWithOptions:opts usingBlock:block];
}

#pragma mark URL Params

- (NSString *)URLEncodedStringValue
{
    return [self URLEncodedStringValueWithOptions:TNLURLEncodingOptionsNone];
}

- (NSString *)stableURLEncodedStringValue
{
    return [self URLEncodedStringValueWithOptions:TNLURLEncodingOptionStableOrder];
}

- (NSDictionary<NSString *, id> *)underlyingDictionaryValue
{
    return [_parameters copy];
}

- (NSDictionary<NSString *, id> *)encodableDictionaryValue
{
    const TNLURLEncodableDictionaryOptions options = TNLURLEncodableDictionaryOptionReplaceArraysWithArraysOfEncodableStrings |
                                                     TNLURLEncodableDictionaryOptionReplaceDictionariesWithDictionariesOfEncodableStrings;
    return [self encodableDictionaryValueWithOptions:options];
}

- (NSDictionary<NSString *, id> *)encodableDictionaryValueWithOptions:(TNLURLEncodableDictionaryOptions)options
{
    return TNLURLEncodableDictionary(_parameters, options);
}

+ (NSString *)stringByCombiningParameterString:(nullable TNLParameterCollection *)parameterStringCollection
                                         query:(nullable TNLParameterCollection *)queryCollection
                                      fragment:(nullable TNLParameterCollection *)fragmentCollection
                                       options:(TNLURLEncodingOptions)options
{
    NSString *parameterString;
    NSString *query;
    NSString *fragment;

    if (parameterStringCollection) {
        parameterString = [parameterStringCollection URLEncodedStringValueWithOptions:options];
        TNLAssert(parameterString);
    }
    if (queryCollection) {
        query = [queryCollection URLEncodedStringValueWithOptions:options];
        TNLAssert(query);
    }
    if (fragmentCollection) {
        fragment = [fragmentCollection URLEncodedStringValueWithOptions:options];
        TNLAssert(fragment);
    }

    NSMutableString *string = [NSMutableString string];

    if (parameterString.length > 0) {
        [string appendString:@";"];
        [string appendString:parameterString];
    }
    if (query.length > 0) {
        [string appendString:@"?"];
        [string appendString:query];
    }
    if (fragment.length > 0) {
        [string appendString:@"#"];
        [string appendString:fragment];
    }

    return string;
}

- (NSString *)URLEncodedStringValueWithOptions:(TNLURLEncodingOptions)options
{
    return TNLURLEncodeDictionary(_parameters, options);
}

#pragma mark Description

- (NSString *)description
{
    return [NSString stringWithFormat:@"<%@ %p: %@='%@'>", NSStringFromClass([self class]), self, NSStringFromSelector(@selector(URLEncodedStringValue)), [self URLEncodedStringValueWithOptions:TNLURLEncodingOptionsNone | TNLURLEncodingOptionTreatUnsupportedValuesAsEmpty]];
}

#pragma mark Equivalence

- (NSUInteger)hash
{
    return _parameters.hash;
}

- (BOOL)isEqual:(id)object
{
    if ([super isEqual:object]) {
        return YES;
    }

    if ([object isKindOfClass:[TNLParameterCollection class]]) {
        return [_parameters isEqualToDictionary:((TNLParameterCollection *)object)->_parameters];
    }

    return NO;
}

@end

TNL_OBJC_DIRECT_MEMBERS
@implementation TNLMutableParameterCollection

- (instancetype)init
{
    return [self initWithCapacity:0];
}

- (instancetype)initWithCapacity:(NSUInteger)capacity
{
    if (self = [super init]) {
        _parameters = capacity ? [[NSMutableDictionary alloc] initWithCapacity:capacity] : [[NSMutableDictionary alloc] init];
    }
    return self;
}

- (instancetype)initWithURLEncodedString:(nullable NSString *)params
                                 options:(TNLURLDecodingOptions)options
{
    if (self = [self init]) {
        [self addParametersWithURLEncodedString:params options:options];
    }
    return self;
}

- (instancetype)initWithDictionary:(nullable NSDictionary<NSString *, id> *)dictionary
{
    if (self = [self initWithCapacity:dictionary.count]) {
        [self _addParametersDirectly:dictionary combineRepeatingKeys:NO];
    }
    return self;
}

- (instancetype)initWithURL:(nullable NSURL *)URL
      parsingParameterTypes:(TNLParameterTypes)types
                    options:(TNLURLDecodingOptions)options
{
    if (self = [self init]) {
        [self addParametersFromURL:URL parsingParameterTypes:types options:options];
    }
    return self;
}

- (instancetype)initWithParameterCollection:(nullable TNLParameterCollection *)otherCollection
{
    if (self = [super init]) {
        _parameters = [[NSMutableDictionary alloc] initWithDictionary:otherCollection.parameters];
    }
    return self;
}

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
    NSDictionary *d = [aDecoder decodeObjectOfClass:[NSMutableDictionary class]
                                             forKey:kParametersCodingKey];
    if (!d) {
        d = [[NSMutableDictionary alloc] init];
    }
    return [self initWithDirectlyAssignedDictionary:d];
}

- (id)copyWithZone:(nullable NSZone *)zone
{
    return [[TNLParameterCollection alloc] initWithParameterCollection:self];
}

- (void)addParametersWithURLEncodedString:(nullable NSString *)params
{
    [self addParametersWithURLEncodedString:params options:TNLURLDecodingOptionsNone];
}

- (void)addParametersWithURLEncodedString:(nullable NSString *)params
                                  options:(TNLURLDecodingOptions)options
{
    const BOOL combine = TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLDecodingOptionCombineRepeatingKeysIntoArray);
    [self _addParametersDirectly:TNLURLDecodeDictionary(params, options)
            combineRepeatingKeys:combine];
}

- (void)addParametersFromURL:(nullable NSURL *)URL parsingParameterTypes:(TNLParameterTypes)types
{
    [self addParametersFromURL:URL
         parsingParameterTypes:types
                       options:TNLURLDecodingOptionsNone];
}

- (void)addParametersFromURL:(nullable NSURL *)URL
       parsingParameterTypes:(TNLParameterTypes)types
                     options:(TNLURLDecodingOptions)options
{
    if (TNL_BITMASK_HAS_SUBSET_FLAGS(types, TNLParameterTypeURLParameterString)) {
        NSString *parameterString;
        if (tnl_available_ios_13) {
            // parameter string is no longer considered valid according to Apple, which is wise
            // ... but we'll still support parsing it
            NSString *path = URL.path;
            if (path) {
                NSRange range = [path rangeOfString:@";"];
                if (range.location != NSNotFound) {
                    parameterString = [path substringFromIndex:range.location + 1];
                }
            }
#if !TARGET_OS_MACCATALYST
        } else {
            parameterString = URL.parameterString;
#endif
        }
        [self addParametersWithURLEncodedString:parameterString options:options];
    }
    if (TNL_BITMASK_HAS_SUBSET_FLAGS(types, TNLParameterTypeURLQuery)) {
        [self addParametersWithURLEncodedString:URL.query options:options];
    }
    if (TNL_BITMASK_HAS_SUBSET_FLAGS(types, TNLParameterTypeURLFragment)) {
        [self addParametersWithURLEncodedString:URL.fragment options:options];
    }
}

- (void)addParametersDirectlyFromDictionary:(nullable NSDictionary<NSString *,id> *)dictionary
                       combineRepeatingKeys:(BOOL)combine
{
    [self _addParametersDirectly:dictionary combineRepeatingKeys:combine];
}

- (void)addParametersFromDictionary:(nullable NSDictionary<NSString *, id> *)dictionary
                 withFormattingMode:(TNLParameterCollectionAddParametersFromDictionaryMode)mode
               combineRepeatingKeys:(BOOL)combine
                             forKey:(NSString *)key
{
    if (!dictionary) {
        return;
    }

    switch (mode) {
        case TNLParameterCollectionAddParametersFromDictionaryModeUseKeysDirectly:
            [self _addParametersDirectly:dictionary combineRepeatingKeys:combine];
            return;
        case TNLParameterCollectionAddParametersFromDictionaryModeURLEncoded:
            [self _addParametersUsingURLEncoding:dictionary combineRepeatingKeys:combine dictionaryKey:key];
            return;
        case TNLParameterCollectionAddParametersFromDictionaryModeJSONEncoded:
            [self _addParametersUsingJSONEncoding:dictionary combineRepeatingKeys:combine dictionaryKey:key];
            return;
        case TNLParameterCollectionAddParametersFromDictionaryModeDotSyntaxOnProvidedKey:
            [self _addParametersUsingDotSyntax:dictionary combineRepeatingKeys:combine dictionaryKey:key];
            return;
    }

    TNLAssertNever();
}

- (void)_setObject:(id)obj forKey:(NSString *)key combineRepeatingKeys:(BOOL)combine
{
    if (combine) {
        id oldValue = _parameters[key];
        if ([oldValue isKindOfClass:[NSString class]]) {
            obj = @[ oldValue, obj ];
        } else if ([oldValue isKindOfClass:[NSArray class]]) {
            oldValue = [oldValue mutableCopy];
            [(NSMutableArray *)oldValue addObject:obj];
            obj = oldValue;
        }
    }

    self[key] = obj;
}

- (void)_updateWithDictionary:(nullable NSDictionary<NSString *, id> *)dictionary
         combineRepeatingKeys:(BOOL)combine
            iterativeKeyBlock:(TNLParameterCollectionUpdateKeysAndValuesIterativeKeyBlock)iterativeKeyBlock
{
    [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
        if (![key isKindOfClass:[NSString class]]) {
            @throw [NSException exceptionWithName:NSInvalidArgumentException
                                           reason:@"keys must be NSStrings for TNLParameterCollection"
                                         userInfo:@{ @"key" : key, @"value" : obj }];
        }
        if ([obj respondsToSelector:@selector(copyWithZone:)]) {
            obj = [obj copy];
            TNLAssert(obj != nil);
        }

        NSString *newKey = iterativeKeyBlock(key, obj);
        TNLAssert(newKey != nil);

        [self _setObject:obj
                  forKey:newKey
    combineRepeatingKeys:combine];
    }];
}

- (void)_addParametersDirectly:(nullable NSDictionary<NSString *, id> *)dictionary
          combineRepeatingKeys:(BOOL)combine
{
    [self _updateWithDictionary:dictionary
           combineRepeatingKeys:combine
              iterativeKeyBlock:^NSString *(NSString *key, id obj) {
        return key;
    }];
}

- (void)_addParametersUsingURLEncoding:(nullable NSDictionary<NSString *, id> *)dictionary
                  combineRepeatingKeys:(BOOL)combine
                         dictionaryKey:(NSString *)key
{
    dictionary = TNLURLEncodableDictionary(dictionary, TNLURLEncodableDictionaryOptionReplaceArraysWithArraysOfEncodableStrings | TNLURLEncodableDictionaryOptionReplaceDictionariesWithDictionariesOfEncodableStrings);
    NSString *obj = TNLURLEncodeDictionary(dictionary, TNLURLEncodingOptionStableOrder);
    [self _setObject:obj forKey:key combineRepeatingKeys:combine];
}

- (void)_addParametersUsingJSONEncoding:(nullable NSDictionary<NSString *, id> *)dictionary
                   combineRepeatingKeys:(BOOL)combine
                          dictionaryKey:(NSString *)key
{
    dictionary = TNLURLEncodableDictionary(dictionary, TNLURLEncodableDictionaryOptionReplaceArraysWithArraysOfEncodableStrings | TNLURLEncodableDictionaryOptionReplaceDictionariesWithDictionariesOfEncodableStrings);
    NSJSONWritingOptions options = 0;
    if (tnl_available_ios_11) {
        options = NSJSONWritingSortedKeys;
    }
    NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary
                                                   options:options
                                                     error:NULL];
    NSString *json = [[NSString alloc] initWithData:data
                                           encoding:NSUTF8StringEncoding];
    [self _setObject:json forKey:key combineRepeatingKeys:combine];
}

- (void)_addParametersUsingDotSyntax:(nullable NSDictionary<NSString *, id> *)dictionary
                combineRepeatingKeys:(BOOL)combine
                       dictionaryKey:(NSString *)key
{
    [self _updateWithDictionary:dictionary
           combineRepeatingKeys:combine
              iterativeKeyBlock:^NSString *(NSString * _Nonnull iterKey, id  _Nonnull obj) {
        return [NSString stringWithFormat:@"%@.%@", key, iterKey];
    }];
}

- (void)addParametersFromParameterCollection:(nullable TNLParameterCollection *)params
                        combineRepeatingKeys:(BOOL)combine
{
    [self _addParametersDirectly:params.underlyingDictionaryValue combineRepeatingKeys:combine];
}

- (void)addParametersFromParameterCollection:(nullable TNLParameterCollection *)params
{
    [self addParametersFromParameterCollection:params combineRepeatingKeys:NO];
}

- (void)setObject:(nullable id)obj forKeyedSubscript:(NSString *)key
{
    [self setParameterValue:obj forKey:key];
}

- (void)setParameterValue:(nullable id)obj forKey:(NSString *)key
{
    if (!obj) {
        [((NSMutableDictionary *)_parameters) removeObjectForKey:key];
        return;
    }

    if (![key isKindOfClass:[NSString class]]) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:@"keys must be NSStrings for TNLParameterCollection"
                                     userInfo:@{ @"key" : key ?: [NSNull null], @"value" : obj }];
    } else if (key.length == 0) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:@"keys cannot be empty strings for TNLParameterCollection"
                                     userInfo:@{ @"key" : key, @"value" : obj }];
    }

    if ([obj respondsToSelector:@selector(copyWithZone:)]) {
        obj = [obj copy];
        TNLAssert(obj != nil);
    }
    ((NSMutableDictionary *)_parameters)[key] = obj;
}

- (void)removeAllParameters
{
    [(NSMutableDictionary *)_parameters removeAllObjects];
}

@end

@implementation NSURL (Parameters)

- (TNLParameterCollection *)tnl_parameterStringCollection
{
    return [self tnl_parameterStringCollectionWithOptions:TNLURLDecodingOptionsNone];
}

- (TNLParameterCollection *)tnl_parameterStringCollectionWithOptions:(TNLURLDecodingOptions)options
{
    return [[TNLParameterCollection alloc] initWithURL:self
                                 parsingParameterTypes:TNLParameterTypeURLParameterString
                                               options:options];
}

- (TNLParameterCollection *)tnl_queryCollection
{
    return [self tnl_queryCollectionWithOptions:TNLURLDecodingOptionsNone];
}

- (TNLParameterCollection *)tnl_queryCollectionWithOptions:(TNLURLDecodingOptions)options
{
    return [[TNLParameterCollection alloc] initWithURL:self
                                 parsingParameterTypes:TNLParameterTypeURLQuery
                                               options:options];
}

- (TNLParameterCollection *)tnl_fragmentCollection
{
    return [self tnl_fragmentCollectionWithOptions:TNLURLDecodingOptionsNone];
}

- (TNLParameterCollection *)tnl_fragmentCollectionWithOptions:(TNLURLDecodingOptions)options
{
    return [[TNLParameterCollection alloc] initWithURL:self
                                 parsingParameterTypes:TNLParameterTypeURLFragment
                                               options:options];
}

@end

NS_ASSUME_NONNULL_END
