///
/// Copyright (c) 2016 Dropbox, Inc. All rights reserved.
///

#import "DBStoneSerializers.h"
#import "DBStoneValidators.h"

static NSDateFormatter *sFormatter = nil;
static NSString *sDateFormat = nil;

@implementation DBNSDateSerializer

+ (void)initialize
{
    if (self == [DBNSDateSerializer class]) {
        sFormatter = [[NSDateFormatter alloc] init];
        [sFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
        [sFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
    }
}

+ (NSString *)serialize:(NSDate *)value dateFormat:(NSString *)dateFormat
{
    if (value == nil) {
        [DBStoneValidators raiseIllegalStateErrorWithMessage:@"Value must not be `nil`"];
    }
    @synchronized (sFormatter) {
        if (![dateFormat isEqualToString:sDateFormat]) {
            [sFormatter setDateFormat:[self convertFormat:dateFormat]];
            sDateFormat = [dateFormat copy];
        }
        return [sFormatter stringFromDate:value];
    }
}

+ (NSDate *)deserialize:(NSString *)value dateFormat:(NSString *)dateFormat
{
    if (value == nil) {
        [DBStoneValidators raiseIllegalStateErrorWithMessage:@"Value must not be `nil`"];
    }
    @synchronized (sFormatter) {
        if (![dateFormat isEqualToString:sDateFormat]) {
            [sFormatter setDateFormat:[self convertFormat:dateFormat]];
            sDateFormat = [dateFormat copy];
        }
        return [sFormatter dateFromString:value];
    }
}

+ (NSString *)formatDateToken:(NSString *)token
{
    NSString *result = @"";

    if ([token isEqualToString:@"%a"]) {  // Weekday as locale's abbreviated name.
        result = @"EEE";
    } else if ([token isEqualToString:@"%A"]) {  // Weekday as locale's full name.
        result = @"EEE";
    } else if ([token isEqualToString:@"%w"]) {  // Weekday as a decimal number, where 0 is Sunday and 6 is Saturday. 0, 1,
        // ..., 6
        result = @"ccccc";
    } else if ([token isEqualToString:@"%d"]) {  // Day of the month as a zero-padded decimal number. 01, 02, ..., 31
        result = @"dd";
    } else if ([token isEqualToString:@"%b"]) {  // Month as locale's abbreviated name.
        result = @"MMM";
    } else if ([token isEqualToString:@"%B"]) {  // Month as locale's full name.
        result = @"MMMM";
    } else if ([token isEqualToString:@"%m"]) {  // Month as a zero-padded decimal number. 01, 02, ..., 12
        result = @"MM";
    } else if ([token isEqualToString:@"%y"]) {  // Year without century as a zero-padded decimal number. 00, 01, ..., 99
        result = @"yy";
    } else if ([token isEqualToString:@"%Y"]) {  // Year with century as a decimal number. 1970, 1988, 2001, 2013
        result = @"yyyy";
    } else if ([token isEqualToString:@"%H"]) {  // Hour (24-hour clock) as a zero-padded decimal number. 00, 01, ..., 23
        result = @"HH";
    } else if ([token isEqualToString:@"%I"]) {  // Hour (12-hour clock) as a zero-padded decimal number. 01, 02, ..., 12
        result = @"hh";
    } else if ([token isEqualToString:@"%p"]) {  // Locale's equivalent of either AM or PM.
        result = @"a";
    } else if ([token isEqualToString:@"%M"]) {  // Minute as a zero-padded decimal number. 00, 01, ..., 59
        result = @"mm";
    } else if ([token isEqualToString:@"%S"]) {  // Second as a zero-padded decimal number. 00, 01, ..., 59
        result = @"ss";
    } else if ([token isEqualToString:@"%f"]) {  // Microsecond as a decimal number, zero-padded on the left. 000000,
        // 000001, ..., 999999
        result = @"SSSSSS";
    } else if ([token isEqualToString:@"%z"]) {  // UTC offset in the form +HHMM or -HHMM (empty string if the the object
        // is naive). (empty), +0000, -0400, +1030
        result = @"Z";
    } else if ([token isEqualToString:@"%Z"]) {  // Time zone name (empty string if the object is naive). (empty), UTC,
        // EST, CST
        result = @"z";
    } else if ([token isEqualToString:@"%j"]) {  // Day of the year as a zero-padded decimal number. 001, 002, ..., 366
        result = @"DDD";
    } else if ([token isEqualToString:@"%U"]) {  // Week number of the year (Sunday as the first day of the week) as a zero
        // padded decimal number. All days in a new year preceding the first
        // Sunday are considered to be in week 0. 00, 01, ..., 53 (6)
        result = @"ww";
    } else if ([token isEqualToString:@"%W"]) {  // Week number of the year (Monday as the first day of the week) as a
        // decimal number. All days in a new year preceding the first Monday are
        // considered to be in week 0. 00, 01, ..., 53 (6)
        result = @"ww";
    } else if ([token isEqualToString:@"%c"]) {  // Locale's appropriate date and time representation.
        result = @"";                         // unsupported
    } else if ([token isEqualToString:@"%x"]) {  // Locale's appropriate date representation.
        result = @"";                         // unsupported
    } else if ([token isEqualToString:@"%X"]) {  // Locale's appropriate time representation.
        result = @"";                         // unsupported
    } else if ([token isEqualToString:@"%%"]) {  // A literal '%' character.
        result = @"";
    } else if ([token isEqualToString:@"%"]) {
        result = @"";
    } else {
        result = @"";
    }

    return result;
}

+ (NSString *)convertFormat:(NSString *)format
{
    NSCharacterSet *alphabeticSet =
    [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
    NSMutableString *newFormat = [@"" mutableCopy];
    BOOL inQuotedText = NO;

    NSUInteger len = [format length];

    NSUInteger i = 0;
    while (i < len) {
        char ch = (char)[format characterAtIndex:i];
        if (ch == '%') {
            if (i >= len - 1) {
                return nil;
            }
            i++;
            ch = (char)[format characterAtIndex:i];
            NSString *token = [NSString stringWithFormat:@"%%%c", ch];
            if (inQuotedText) {
                [newFormat appendString:@"'"];
                inQuotedText = NO;
            }
            [newFormat appendString:[self formatDateToken:token]];
        } else {
            if ([alphabeticSet characterIsMember:ch]) {
                if (!inQuotedText) {
                    [newFormat appendString:@"'"];
                    inQuotedText = YES;
                }
            } else if (ch == '\'') {
                [newFormat appendString:@"'"];
            }
            [newFormat appendString:[NSString stringWithFormat:@"%c", ch]];
        }
        i++;
    }

    if (inQuotedText) {
        [newFormat appendString:@"'"];
    }

    return newFormat;
}

@end

@implementation DBArraySerializer

+ (NSArray *)serialize:(NSArray *)value withBlock:(id (^)(id))serializeBlock
{
    if (value == nil) {
        [DBStoneValidators raiseIllegalStateErrorWithMessage:@"Value must not be `nil`"];
    }
    NSMutableArray *resultArray = [[NSMutableArray alloc] init];

    for (id element in value) {
        [resultArray addObject:serializeBlock(element)];
    }

    return resultArray;
}

+ (NSArray *)deserialize:(NSArray *)value withBlock:(id (^)(id))deserializeBlock
{
    if (value == nil) {
        [DBStoneValidators raiseIllegalStateErrorWithMessage:@"Value must not be `nil`"];
    }
    NSMutableArray *resultArray = [[NSMutableArray alloc] init];

    for (id element in value) {
        [resultArray addObject:deserializeBlock(element)];
    }

    return resultArray;
}

@end

@implementation DBMapSerializer

+ (NSDictionary *)serialize:(NSDictionary *)value withBlock:(id (^)(id))serializeBlock
{
    if (value == nil) {
        [DBStoneValidators raiseIllegalStateErrorWithMessage:@"Value must not be `nil`"];
    }
    NSMutableDictionary *resultDict = [[NSMutableDictionary alloc] init];

    for (id key in value) {
        [resultDict setObject:serializeBlock(value[key]) forKey:key];
    }

    return resultDict;
}

+ (NSDictionary *)deserialize:(NSDictionary *)value withBlock:(id (^)(id))deserializeBlock
{
    if (value == nil) {
        [DBStoneValidators raiseIllegalStateErrorWithMessage:@"Value must not be `nil`"];
    }
    NSMutableDictionary *resultDict = [[NSMutableDictionary alloc] init];

    for (id key in value) {
        [resultDict setObject:deserializeBlock(value[key]) forKey:key];
    }

    return resultDict;
}

@end
