#import "{{classPrefix}}ResponseDeserializer.h"
#import <JSONModel/JSONModel.h>
#import <ISO8601/ISO8601.h>

NSString *const {{classPrefix}}DeserializationErrorDomainKey = @"{{classPrefix}}DeserializationErrorDomainKey";

NSInteger const {{classPrefix}}TypeMismatchErrorCode = 143553;

NSInteger const {{classPrefix}}EmptyValueOccurredErrorCode = 143509;

NSInteger const {{classPrefix}}UnknownResponseObjectErrorCode = 143528;


@interface {{classPrefix}}ResponseDeserializer ()

@property (nonatomic, strong) NSNumberFormatter* numberFormatter;
@property (nonatomic, strong) NSArray *primitiveTypes;
@property (nonatomic, strong) NSArray *basicReturnTypes;
@property (nonatomic, strong) NSArray *dataReturnTypes;

@property (nonatomic, strong) NSRegularExpression* arrayOfModelsPatExpression;
@property (nonatomic, strong) NSRegularExpression* arrayOfPrimitivesPatExpression;
@property (nonatomic, strong) NSRegularExpression* dictPatExpression;
@property (nonatomic, strong) NSRegularExpression* dictModelsPatExpression;

@end

@implementation {{classPrefix}}ResponseDeserializer

- (instancetype)init {
    self = [super init];
    if (self) {
        NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
        formatter.numberStyle = NSNumberFormatterDecimalStyle;
        _numberFormatter = formatter;
        _primitiveTypes = @[NSStringFromClass(NSString.class), NSStringFromClass(NSDate.class), NSStringFromClass(NSNumber.class)];
        _basicReturnTypes = @[NSStringFromClass(NSObject.class), @"id"];
        _dataReturnTypes = @[NSStringFromClass(NSData.class)];

        _arrayOfModelsPatExpression = [NSRegularExpression regularExpressionWithPattern:@"NSArray<(.+)>"
                                                                                options:NSRegularExpressionCaseInsensitive
                                                                                  error:nil];
        _arrayOfPrimitivesPatExpression = [NSRegularExpression regularExpressionWithPattern:@"NSArray\\* /\\* (.+) \\*/"
                                                                                    options:NSRegularExpressionCaseInsensitive
                                                                                      error:nil];
        _dictPatExpression = [NSRegularExpression regularExpressionWithPattern:@"NSDictionary\\* /\\* (.+?), (.+) \\*/"
                                                                       options:NSRegularExpressionCaseInsensitive
                                                                         error:nil];
        _dictModelsPatExpression = [NSRegularExpression regularExpressionWithPattern:@"NSDictionary\\<(.+?), (.+)*\\>"
                                                                             options:NSRegularExpressionCaseInsensitive
                                                                               error:nil];
    }
    return self;
}

#pragma mark - private method
#pragma mark -

- (id)_deserializeDictionaryValue:(id)data 
                        valueType:(NSString *)valueType 
                            error:(NSError **)error
{
    if([data isKindOfClass:NSDictionary.class] == NO) {
        if(error) {
            *error = [self _typeMismatchErrorWithExpectedType:NSStringFromClass(NSDictionary.class) data:data];
        }
        return nil;
    }

    __block NSMutableDictionary *resultDict = [NSMutableDictionary dictionaryWithCapacity:[(NSDictionary *)data count]];
    for (id key in [(NSDictionary *)data allKeys]) {
        id obj = [data valueForKey:key];
        id dicObj = [self deserialize:obj class:valueType error:error];
        if (dicObj) {
            [resultDict setValue:dicObj forKey:key];
        } else if ([obj isKindOfClass:NSNull.class]) {
            if(self.treatNullAsError) {
                if (error) {
                   *error = [self _emptyValueOccurredError];
                }
                resultDict = nil;
                break;
            }
        } else {
            resultDict = nil;
            break;
        }
    }

    return resultDict;
}

- (id)_deserializePrimitiveValue:(id)data 
                           class:(NSString *)className 
                           error:(NSError **)error
{
    if ([className isEqualToString:NSStringFromClass(NSString.class)]) {
        return [NSString stringWithFormat:@"%@",data];
    } else if ([className isEqualToString:NSStringFromClass(NSDate.class)]) {
        return [self _deserializeDateValue:data error:error];
    } else if ([className isEqualToString:NSStringFromClass(NSNumber.class)]) {
        // NSNumber from NSNumber
        if ([data isKindOfClass:NSNumber.class]) {
            return data;
        } else if ([data isKindOfClass:NSString.class]) {
            // NSNumber (NSCFBoolean) from NSString
            if ([[data lowercaseString] isEqualToString:@"true"] || [[data lowercaseString] isEqualToString:@"false"]) {
                return @([data boolValue]);
                // NSNumber from NSString
            } else {
                NSNumber* formattedValue = [self.numberFormatter numberFromString:data];
                if (!formattedValue && [data isKindOfClass:NSString.class] && ((NSString *)data).length > 0 && error) {
                    *error = [self _typeMismatchErrorWithExpectedType:className data:data];
                }
                return formattedValue;
            }
        }
    }

    if(error) {
        *error = [self _typeMismatchErrorWithExpectedType:className data:data];
    }

    return nil;
}

- (id)_deserializeDateValue:(id)data 
                      error:(NSError **)error
{
    NSDate *date =[NSDate dateWithISO8601String:data];

    if (!date && [data isKindOfClass:NSString.class] && ((NSString *)data).length > 0 && error) {
        *error = [self _typeMismatchErrorWithExpectedType:NSStringFromClass([NSDate class]) data:data];
    }

    return date;
};

- (NSError *)_typeMismatchErrorWithExpectedType:(NSString *)expected 
                                           data:(id)data
{
    NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Received response [%@] is not an object of type %@",nil), data, expected];
    NSDictionary *userInfo = @{NSLocalizedDescriptionKey: message};
    return [NSError errorWithDomain:{{classPrefix}}DeserializationErrorDomainKey code:{{classPrefix}}TypeMismatchErrorCode userInfo:userInfo];
}

- (NSError *)_emptyValueOccurredError
{
    NSString *message = NSLocalizedString(@"Received response contains null value in dictionary or array response", nil);
    NSDictionary *userInfo = @{NSLocalizedDescriptionKey: message};
    return [NSError errorWithDomain:{{classPrefix}}DeserializationErrorDomainKey code:{{classPrefix}}EmptyValueOccurredErrorCode userInfo:userInfo];
}

- (NSError *)_unknownResponseErrorWithExpectedType:(NSString *)expected 
                                              data:(id)data
{
    NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Unknown response expected type %@ [response: %@]", nil), expected, data];
    NSDictionary *userInfo = @{NSLocalizedDescriptionKey : message};
    return [NSError errorWithDomain:{{classPrefix}}DeserializationErrorDomainKey code:{{classPrefix}}UnknownResponseObjectErrorCode userInfo:userInfo];
}

#pragma mark - public method
#pragma mark -

- (id)deserialize:(id)data 
            class:(NSString *)className 
            error:(NSError **)error
{
    if (!data || !className) {
        return nil;
    }

    if ([className hasSuffix:@"*"]) {
        className = [className substringToIndex:[className length] - 1];
    }

    if ([self.dataReturnTypes containsObject:className]) {
        return data;
    }

    id jsonData = nil;
    if ([data isKindOfClass:[NSData class]]) {
        jsonData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:error];
    } else {
        jsonData = data;
    }

    // special model
    id model = [self deserializeSpecialModel:jsonData class:className error:error];
    if (model) {
        return model;
    }

    if (!jsonData) {
        jsonData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    } else if([jsonData isKindOfClass:[NSNull class]]) {
        return nil;
    }

    // pure object
    if ([self.basicReturnTypes containsObject:className]) {
        return jsonData;
    }

    // primitives
    if ([self.primitiveTypes containsObject:className]) {
        return [self _deserializePrimitiveValue:jsonData class:className error:error];
    }

    NSTextCheckingResult *match = nil;
    NSRange range = NSMakeRange(0, [className length]);
    // list of models
    match = [self.arrayOfModelsPatExpression firstMatchInString:className options:0 range:range];
    if (match) {
        NSString *innerType = [className substringWithRange:[match rangeAtIndex:1]];
        return [self deserializeArrayValue:jsonData innerType:innerType error:error];
    }

    // list of primitives
    match = [self.arrayOfPrimitivesPatExpression firstMatchInString:className options:0 range:range];
    if (match) {
        NSString *innerType = [className substringWithRange:[match rangeAtIndex:1]];
        return [self deserializeArrayValue:jsonData innerType:innerType error:error];
    }

    // map
    match = [self.dictPatExpression firstMatchInString:className options:0 range:range];
    if (match) {
        NSString *valueType = [className substringWithRange:[match rangeAtIndex:2]];
        return [self _deserializeDictionaryValue:jsonData valueType:valueType error:error];
    }

    match = [self.dictModelsPatExpression firstMatchInString:className options:0 range:range];
    if (match) {
        NSString *valueType = [className substringWithRange:[match rangeAtIndex:2]];
        return [self _deserializeDictionaryValue:jsonData valueType:valueType error:error];
    }

    // model
    Class ModelClass = NSClassFromString(className);
    if ([ModelClass instancesRespondToSelector:@selector(initWithDictionary:error:)]) {
        return [(JSONModel *) [ModelClass alloc] initWithDictionary:jsonData error:error];
    }

    if(error) {
        *error = [self _unknownResponseErrorWithExpectedType:className data:jsonData];
    }

    return nil;
}

- (id)deserializeSpecialModel:(NSDictionary *)dic
					    class:(NSString *)className
                        error:(NSError **)error
{
    return nil;
}

- (id)deserializeArrayValue:(id)data 
                  innerType:(NSString *)innerType 
                      error:(NSError **)error
{
    if([data isKindOfClass:NSArray.class] == NO) {
        if(error) {
            *error = [self _typeMismatchErrorWithExpectedType:NSStringFromClass([NSArray class]) data:data];
        }
        return nil;
    }

    NSMutableArray* resultArray = [NSMutableArray arrayWithCapacity:[(NSArray *)data count]];
    for (id obj in data) {
        id arrObj = [self deserialize:obj class:innerType error:error];
        if (arrObj) {
            [resultArray addObject:arrObj];
        } else if ([obj isKindOfClass:NSNull.class]) {
            if(self.treatNullAsError) {
                if (error) {
                    *error = [self _emptyValueOccurredError];
                }
                resultArray = nil;
                break;
            }
        } else {
            resultArray = nil;
            break;
        }
    }
    return resultArray;
};

@end
