/*
 *  Copyright (c) 2014, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 *
 */

#import <UIKit/UIKit.h>

#import "BFWebViewAppLinkResolver.h"
#import "BFAppLink.h"
#import "BFAppLinkTarget.h"
#import "BFTask.h"
#import "BFTaskCompletionSource.h"
#import "BFExecutor.h"

// Defines JavaScript to extract app link tags from HTML content
static NSString *const BFWebViewAppLinkResolverTagExtractionJavaScript = @""
"(function() {"
"  var metaTags = document.getElementsByTagName('meta');"
"  var results = [];"
"  for (var i = 0; i < metaTags.length; i++) {"
"    var property = metaTags[i].getAttribute('property');"
"    if (property && property.substring(0, 'al:'.length) === 'al:') {"
"      var tag = { \"property\": metaTags[i].getAttribute('property') };"
"      if (metaTags[i].hasAttribute('content')) {"
"        tag['content'] = metaTags[i].getAttribute('content');"
"      }"
"      results.push(tag);"
"    }"
"  }"
"  return JSON.stringify(results);"
"})()";
static NSString *const BFWebViewAppLinkResolverIOSURLKey = @"url";
static NSString *const BFWebViewAppLinkResolverIOSAppStoreIdKey = @"app_store_id";
static NSString *const BFWebViewAppLinkResolverIOSAppNameKey = @"app_name";
static NSString *const BFWebViewAppLinkResolverDictionaryValueKey = @"_value";
static NSString *const BFWebViewAppLinkResolverPreferHeader = @"Prefer-Html-Meta-Tags";
static NSString *const BFWebViewAppLinkResolverMetaTagPrefix = @"al";
static NSString *const BFWebViewAppLinkResolverWebKey = @"web";
static NSString *const BFWebViewAppLinkResolverIOSKey = @"ios";
static NSString *const BFWebViewAppLinkResolverIPhoneKey = @"iphone";
static NSString *const BFWebViewAppLinkResolverIPadKey = @"ipad";
static NSString *const BFWebViewAppLinkResolverWebURLKey = @"url";
static NSString *const BFWebViewAppLinkResolverShouldFallbackKey = @"should_fallback";

@interface BFWebViewAppLinkResolverWebViewDelegate : NSObject <UIWebViewDelegate>

@property (nonatomic, copy) void (^didFinishLoad)(UIWebView *webView);
@property (nonatomic, copy) void (^didFailLoadWithError)(UIWebView *webView, NSError *error);
@property (nonatomic, assign) BOOL hasLoaded;

@end

@implementation BFWebViewAppLinkResolverWebViewDelegate

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    if (self.didFinishLoad) {
        self.didFinishLoad(webView);
    }
}

- (void)webViewDidStartLoad:(UIWebView *)webView {
}

- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
    if (self.didFailLoadWithError) {
        self.didFailLoadWithError(webView, error);
    }
}

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if (self.hasLoaded) {
        // Consider loading a second resource to be "success", since it indicates an inner frame
        // or redirect is happening. We can run the tag extraction script at this point.
        self.didFinishLoad(webView);
        return NO;
    }
    self.hasLoaded = YES;
    return YES;
}

@end

@implementation BFWebViewAppLinkResolver

+ (instancetype)sharedInstance {
    static id instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

- (BFTask *)followRedirects:(NSURL *)url {
    // This task will be resolved with either the redirect NSURL
    // or a dictionary with the response data to be returned.
    BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setValue:BFWebViewAppLinkResolverMetaTagPrefix forHTTPHeaderField:BFWebViewAppLinkResolverPreferHeader];

    void (^completion)(NSURLResponse *response, NSData *data, NSError *error) = ^(NSURLResponse *response, NSData *data, NSError *error) {
        if (error) {
            [tcs setError:error];
            return;
        }

        if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

            // NSURLConnection usually follows redirects automatically, but the
            // documentation is unclear what the default is. This helps it along.
            if (httpResponse.statusCode >= 300 && httpResponse.statusCode < 400) {
                NSString *redirectString = httpResponse.allHeaderFields[@"Location"];
                NSURL *redirectURL = [NSURL URLWithString:redirectString];
                [tcs setResult:redirectURL];
                return;
            }
        }

        [tcs setResult:@{ @"response" : response, @"data" : data }];
    };

#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9
    NSURLSession *session = [NSURLSession sharedSession];
    [[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        completion(response, data, error);
    }] resume];
#else
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:completion];
#endif

    return [tcs.task continueWithSuccessBlock:^id(BFTask *task) {
        // If we redirected, just keep recursing.
        if ([task.result isKindOfClass:[NSURL class]]) {
            return [self followRedirects:task.result];
        }
        return task;
    }];
}

- (BFTask *)appLinkFromURLInBackground:(NSURL *)url {
    return [[self followRedirects:url] continueWithExecutor:[BFExecutor mainThreadExecutor]
                                           withSuccessBlock:^id(BFTask *task) {
                                               NSData *responseData = task.result[@"data"];
                                               NSHTTPURLResponse *response = task.result[@"response"];
                                               BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];

                                               UIWebView *webView = [[UIWebView alloc] init];
                                               BFWebViewAppLinkResolverWebViewDelegate *listener = [[BFWebViewAppLinkResolverWebViewDelegate alloc] init];
                                               __block BFWebViewAppLinkResolverWebViewDelegate *retainedListener = listener;
                                               listener.didFinishLoad = ^(UIWebView *view) {
                                                   if (retainedListener) {
                                                       NSDictionary *ogData = [self getALDataFromLoadedPage:view];
                                                       [view removeFromSuperview];
                                                       view.delegate = nil;
                                                       retainedListener = nil;
                                                       [tcs setResult:[self appLinkFromALData:ogData destination:url]];
                                                   }
                                               };
                                               listener.didFailLoadWithError = ^(UIWebView* view, NSError *error) {
                                                   if (retainedListener) {
                                                       [view removeFromSuperview];
                                                       view.delegate = nil;
                                                       retainedListener = nil;
                                                       [tcs setError:error];
                                                   }
                                               };
                                               webView.delegate = listener;
                                               webView.hidden = YES;
                                               [webView loadData:responseData
                                                        MIMEType:response.MIMEType
                                                textEncodingName:response.textEncodingName
                                                         baseURL:response.URL];
                                               UIWindow *window = [UIApplication sharedApplication].windows.firstObject;
                                               [window addSubview:webView];

                                               return tcs.task;
                                           }];
}

/*
 Builds up a data structure filled with the app link data from the meta tags on a page.
 The structure of this object is a dictionary where each key holds an array of app link
 data dictionaries.  Values are stored in a key called "_value".
 */
- (NSDictionary *)parseALData:(NSArray *)dataArray {
    NSMutableDictionary *al = [NSMutableDictionary dictionary];
    for (NSDictionary *tag in dataArray) {
        NSString *name = tag[@"property"];
        if (![name isKindOfClass:[NSString class]]) {
            continue;
        }
        NSArray *nameComponents = [name componentsSeparatedByString:@":"];
        if (![nameComponents[0] isEqualToString:BFWebViewAppLinkResolverMetaTagPrefix]) {
            continue;
        }
        NSMutableDictionary *root = al;
        for (int i = 1; i < nameComponents.count; i++) {
            NSMutableArray *children = root[nameComponents[i]];
            if (!children) {
                children = [NSMutableArray array];
                root[nameComponents[i]] = children;
            }
            NSMutableDictionary *child = children.lastObject;
            if (!child || i == nameComponents.count - 1) {
                child = [NSMutableDictionary dictionary];
                [children addObject:child];
            }
            root = child;
        }
        if (tag[@"content"]) {
            root[BFWebViewAppLinkResolverDictionaryValueKey] = tag[@"content"];
        }
    }
    return al;
}

- (NSDictionary *)getALDataFromLoadedPage:(UIWebView *)webView {
    // Run some JavaScript in the webview to fetch the meta tags.
    NSString *jsonString = [webView stringByEvaluatingJavaScriptFromString:BFWebViewAppLinkResolverTagExtractionJavaScript];
    NSError *error = nil;
    NSArray *arr = [NSJSONSerialization JSONObjectWithData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]
                                                   options:0
                                                     error:&error];
    return [self parseALData:arr];
}

/*
 Converts app link data into a BFAppLink containing the targets relevant for this platform.
 */
- (BFAppLink *)appLinkFromALData:(NSDictionary *)appLinkDict destination:(NSURL *)destination {
    NSMutableArray *linkTargets = [NSMutableArray array];

    NSArray *platformData = nil;
    switch (UI_USER_INTERFACE_IDIOM()) {
        case UIUserInterfaceIdiomPad:
            platformData = @[ appLinkDict[BFWebViewAppLinkResolverIPadKey] ?: @{},
                              appLinkDict[BFWebViewAppLinkResolverIOSKey] ?: @{} ];
            break;
        case UIUserInterfaceIdiomPhone:
            platformData = @[ appLinkDict[BFWebViewAppLinkResolverIPhoneKey] ?: @{},
                              appLinkDict[BFWebViewAppLinkResolverIOSKey] ?: @{} ];
            break;
#ifdef __TVOS_9_0
        case UIUserInterfaceIdiomTV:
#endif
#ifdef __IPHONE_9_3
        case UIUserInterfaceIdiomCarPlay:
#endif
        case UIUserInterfaceIdiomUnspecified:
        default:
            // Future-proofing. Other User Interface idioms should only hit ios.
            platformData = @[ appLinkDict[BFWebViewAppLinkResolverIOSKey] ?: @{} ];
            break;
    }

    for (NSArray *platformObjects in platformData) {
        for (NSDictionary *platformDict in platformObjects) {
            // The schema requires a single url/app store id/app name,
            // but we could find multiple of them. We'll make a best effort
            // to interpret this data.
            NSArray *urls = platformDict[BFWebViewAppLinkResolverIOSURLKey];
            NSArray *appStoreIds = platformDict[BFWebViewAppLinkResolverIOSAppStoreIdKey];
            NSArray *appNames = platformDict[BFWebViewAppLinkResolverIOSAppNameKey];

            NSUInteger maxCount = MAX(urls.count, MAX(appStoreIds.count, appNames.count));

            for (NSUInteger i = 0; i < maxCount; i++) {
                NSString *urlString = urls[i][BFWebViewAppLinkResolverDictionaryValueKey];
                NSURL *url = urlString ? [NSURL URLWithString:urlString] : nil;
                NSString *appStoreId = appStoreIds[i][BFWebViewAppLinkResolverDictionaryValueKey];
                NSString *appName = appNames[i][BFWebViewAppLinkResolverDictionaryValueKey];
                BFAppLinkTarget *target = [BFAppLinkTarget appLinkTargetWithURL:url
                                                                     appStoreId:appStoreId
                                                                        appName:appName];
                [linkTargets addObject:target];
            }
        }
    }

    NSDictionary *webDict = appLinkDict[BFWebViewAppLinkResolverWebKey][0];
    NSString *webUrlString = webDict[BFWebViewAppLinkResolverWebURLKey][0][BFWebViewAppLinkResolverDictionaryValueKey];
    NSString *shouldFallbackString = webDict[BFWebViewAppLinkResolverShouldFallbackKey][0][BFWebViewAppLinkResolverDictionaryValueKey];

    NSURL *webUrl = destination;

    if (shouldFallbackString &&
        [@[ @"no", @"false", @"0" ] containsObject:[shouldFallbackString lowercaseString]]) {
        webUrl = nil;
    }
    if (webUrl && webUrlString) {
        webUrl = [NSURL URLWithString:webUrlString];
    }

    return [BFAppLink appLinkWithSourceURL:destination
                                   targets:linkTargets
                                    webURL:webUrl];
}

@end
