#import "GBHub.h"
#pragma clang diagnostic ignored "-Warc-retain-cycles"

NSString *const GBHubStatusChangedNotificationName = @"GBHubStatusChangedNotification";

static NSURL *StringToWebURL(NSString *string)
{
    if (![string isKindOfClass:[NSString class]]) return nil;
    NSURL *url = [NSURL URLWithString:string];
    if (![url.scheme isEqual:@"http"] && [url.scheme isEqual:@"https"]) {
        return nil;
    }
    
    return url;
}

@implementation GBHubGame

- (instancetype)initWithJSON:(NSDictionary *)json
{
    self = [super init];
    
    // Skip NSFW titles
    if ([json[@"nsfw"] boolValue]) return nil;
    
    if (json[@"tags"] && ![json[@"tags"] isKindOfClass:[NSArray class]]) return nil;
    _tags = [NSMutableArray array];

    for (__strong NSString *tag in json[@"tags"]) {
        if (![tag isKindOfClass:[NSString class]]) {
            return nil;
        }
        if ([tag isEqual:@"hw:gbprinter"]) {
            continue;
        }
        
        if ([tag hasPrefix:@"event:"]) {
            tag = [tag substringFromIndex:strlen("event:")];
        }
        if ([tag hasPrefix:@"gb-showdown-"]) {
            tag = [NSString stringWithFormat:@"Game Boy Showdown %@", [tag substringFromIndex:strlen("gb-showdown-")]];
        }
        if ([tag hasPrefix:@"gbcompo"]) {
            tag = [NSString stringWithFormat:@"GBCompo%@", [[tag substringFromIndex:strlen("gbcompo")].capitalizedString stringByReplacingOccurrencesOfString:@"-" withString:@" "]];
        }
        if ([tag isEqual:tag.lowercaseString]) {
            tag = [tag stringByReplacingOccurrencesOfString:@"-" withString:@" "].capitalizedString;
        }
        [(NSMutableArray *)_tags addObject:tag];
    }
    
    NSMutableSet *licenses = [NSMutableSet set];
    
    if (json[@"license"]) {
        [licenses addObject:json[@"license"]];
    }
    if (json[@"gameLicense"]) {
        [licenses addObject:json[@"gameLicense"]];
    }
    if (json[@"assetsLicense"]) {
        [licenses addObject:json[@"assetsLicense"]];
    }
    
    
    if (licenses.count == 1) {
        _license = licenses.anyObject;
        if (![_license isKindOfClass:[NSString class]]) {
            return nil;
        }
        if (!_license.length) _license = nil;
    }
    else if (licenses.count > 1) {
        if (json[@"license"]) {
            return nil;
        }
        _license = [NSString stringWithFormat:@"%@ (Assets: %@)", json[@"gameLicense"], json[@"assetsLicense"]];
    }
    
    if (_license && ![_tags containsObject:@"Open Source"]) {
        // License is guaranteed to be Open Source by spec
        [(NSMutableArray *)_tags addObject:@"Open Source"];
    }
        
    _title = json[@"title"];
    if (![_title isKindOfClass:[NSString class]]) {
        return nil;
    }
    
    _entryDescription = json[@"description"];
    if (_entryDescription && ![_entryDescription isKindOfClass:[NSString class]]) {
        return nil;
    }
    
    _developer = json[@"developer"];
    if (![_developer isKindOfClass:[NSString class]]) {
        if ([_developer isKindOfClass:[NSArray class]] && ((NSArray *)_developer).count) {
            if ([((NSArray *)_developer)[0] isKindOfClass:[NSString class]]) {
                _developer = [(NSArray *)_developer componentsJoinedByString:@", "];
            }
            else if ([((NSArray *)_developer)[0] isKindOfClass:[NSDictionary class]]) {
                NSMutableArray *developers = [NSMutableArray array];
                for (NSDictionary *developer in (NSArray *)_developer) {
                    if (![developer isKindOfClass:[NSDictionary class]]) return nil;
                    NSString *name = developer[@"name"];
                    if (!name) return nil;
                    [developers addObject:name];
                }
                _developer = [developers componentsJoinedByString:@", "];
            }
            else {
                return nil;
            }
        }
        else if ([_developer isKindOfClass:[NSDictionary class]]) {
            _developer = ((NSDictionary *)_developer)[@"name"];
        }
        else {
            return nil;
        }
    }

    
    _slug = json[@"slug"];
    if (![_slug isKindOfClass:[NSString class]]) {
        return nil;
    }

    _type = json[@"typetag"];
    if (![_type isKindOfClass:[NSString class]]) {
        return nil;
    }
    
    NSString *dateString = json[@"date"];
    if ([dateString isKindOfClass:[NSString class]]) {
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@"yyyy-MM-dd"];
        _publicationDate = [dateFormatter dateFromString:dateString];
    }
    
    _repository = StringToWebURL(json[@"repository"]);
    _website = StringToWebURL(json[@"website"]);
    
    NSURL *base = [NSURL URLWithString:[NSString stringWithFormat:@"https://hh3.gbdev.io/static/database-gb/entries/%@", _slug]];
    
    NSMutableArray *screenshots = [NSMutableArray array];
    for (NSString *screenshot in json[@"screenshots"]) {
        [screenshots addObject:[base URLByAppendingPathComponent:screenshot]];
    }
    
    
    _screenshots = screenshots;
    
    for (NSDictionary *file in json[@"files"]) {
        NSString *extension = [file[@"filename"] pathExtension].lowercaseString;
        if (![extension isEqual:@"gb"] && ![extension isEqual:@"gbc"]) {
            // Not a DMG/CGB game
            continue;
        }
        if ([file[@"default"] boolValue] || !_file) {
            _file = [base URLByAppendingPathComponent:file[@"filename"]];
        }
    }
    
    if (!_file) {
        return nil;
    }
    
    _keywords = [NSString stringWithFormat:@"%@ %@ %@ %@",
                 GBSearchCanonicalString(_title),
                 GBSearchCanonicalString(_developer),
                 GBSearchCanonicalString(_entryDescription) ?: @"",
                 GBSearchCanonicalString([_tags componentsJoinedByString:@" "])
    ];
    
    return self;
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"<%@ %p: %@>", self.class, self, self.title];
}

@end

@implementation GBHub
{
    NSMutableDictionary<NSString *, GBHubGame *> *_allGames;
    NSMutableDictionary<NSString *, NSNumber *> *_tags;
    NSMutableArray<GBHubGame *> *_showcaseGames;
    NSSet<NSString *> *_showcaseExtras;
}

+ (instancetype)sharedHub
{
    static GBHub *hub = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        hub = [[self alloc] init];
    });
    return hub;
}

- (void)setStatus:(GBHubStatus)status
{
    if (_status != status) {
        _status = status;
        if ([NSThread isMainThread]) {
            [[NSNotificationCenter defaultCenter] postNotificationName:GBHubStatusChangedNotificationName
                                                                object:self];
        }
        else {
            dispatch_sync(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:GBHubStatusChangedNotificationName
                                                                    object:self];
            });
        }
    }
}

- (void)handleAPIData:(NSData *)data forBaseURL:(NSString *)base completion:(void(^)(GBHubStatus))completion
{
    @try {
        if (!data) {
            completion(GBHubStatusError);
            return;
        }
        NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        if (!json) {
            completion(GBHubStatusError);
            return;
        }
        
        if (!json[@"page_current"] || !json[@"page_total"]) {
            completion(GBHubStatusError);
            return;
        }
        
        for (NSDictionary *entry in json[@"entries"]) {
            @try {
                @autoreleasepool {
                    GBHubGame *game = [[GBHubGame alloc] initWithJSON:entry];
                    if (game && !_allGames[game.slug]) {
                        _allGames[game.slug] = game;
                        bool showcase = [_showcaseExtras containsObject:game.slug];
                        if (!showcase) {
                            for (NSString *tag in game.tags) {
                                _tags[tag] = @(_tags[tag].unsignedIntValue + 1);
                                if ([tag containsString:@"Shortlist"]) {
                                    showcase = true;
                                    break;
                                }
                            }
                        }
                        if (showcase) {
                            [_showcaseGames addObject:game];
                        }
                    }
                }
            }
            @catch (NSException *exception) {
                // Just in case I missed some JSON edge cases, let's not abort the entire response over one bad game
            }
        }
        
        if ([json[@"page_current"] unsignedIntValue] == [json[@"page_total"] unsignedIntValue]) {
            completion(GBHubStatusReady);
            return;
        }
        
        [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@&page=%u",
                                                                             base,
                                                                             [json[@"page_current"] unsignedIntValue] + 1]]
                                     completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
            [self handleAPIData:data forBaseURL:base completion:completion];
        }] resume];
    }
    @catch (NSException *exception) {
        self.status = GBHubStatusError;
    }
}

- (void)addGamesForURL:(NSString *)url completion:(void(^)(GBHubStatus))completion
{
    [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:url]
                                 completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (error) {
            completion(GBHubStatusError);
            return;
        }
        [self handleAPIData:data forBaseURL:url completion:completion];
    }] resume];
}

- (unsigned)countForTag:(NSString *)tag
{
    return _tags[tag].unsignedIntValue;
}

- (void)refresh
{
    if (_status == GBHubStatusInProgress) {
        return;
    }
    self.status = GBHubStatusInProgress;
    _allGames = [NSMutableDictionary dictionary];
    _tags = [NSMutableDictionary dictionary];
    _showcaseGames = [NSMutableArray array];
    
    [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"https://sameboy.github.io/ios-showcase"]
                                 completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (data) {
            _showcaseExtras = [NSSet setWithArray:[[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] componentsSeparatedByString:@"\n"]];
        }
        [self addGamesForURL:@"https://hh3.gbdev.io/api/search?tags=Open+Source&results=1000"
                  completion:^(GBHubStatus ret) {
            if (ret != GBHubStatusReady) {
                self.status = ret;
                return;
            }
            [self addGamesForURL:@"https://hh3.gbdev.io/api/search?thirdparty=sameboy&results=1000"
                      completion:^(GBHubStatus ret) {
                if (ret == GBHubStatusReady) {
                    _sortedTags = [_tags.allKeys sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) {
                        return [obj1 compare:obj2];
                    }];
                }
                unsigned day = time(NULL) / 60 / 60 / 24;
                if (_showcaseGames.count > 5) {
                    typeof(_showcaseGames) temp = [NSMutableArray array];
                    for (unsigned i = 5; i--;) {
                        unsigned index = day % _showcaseGames.count;
                        GBHubGame *game = _showcaseGames[index];
                        [_showcaseGames removeObjectAtIndex:index];
                        [temp addObject:game];
                    }
                    _showcaseGames = temp;
                }
                self.status = ret;
            }];
        }];
    }] resume];
}

@end
