//
//  AstroViewController.swift
//  Astro
//
//  Created by Mark Sandstrom on 4/28/15.
//  Copyright (c) 2015 Mobify Research & Development Inc. All rights reserved.
//

import Cordova

func _mainBundle() -> Bundle {
    let testBundleIdentifier = ProcessInfo.processInfo.environment["TEST_BUNDLE_IDENTIFIER"]

    if let testBundleIdentifier = testBundleIdentifier {
        if let bundle = Bundle(identifier: testBundleIdentifier) {
            return bundle
        }
    }
    return Bundle.main
}

// Basically a marker view so it's easy to tell in the view debugger which view is the AstroWorker
class AstroWorkerView: UIView {}

// This class mainly exists to allow childViewControllers to control the visibility
// of the status bar when replacing the status bar with a snapshot. It also updates
// the appearance when the statusBarSyle property is set.
public class StatusBarStateViewController: UIViewController {
    private var _childViewControllerForStatusBarHidden: UIViewController?
    private var _childViewControllerForStatusBarStyle: UIViewController?

    @objc var statusBarStyle = UIStatusBarStyle.default {
        didSet {
            setNeedsStatusBarAppearanceUpdate()
        }
    }

    @objc func setChildViewControllerForStatusBarHidden(_ viewController: UIViewController) {
        _childViewControllerForStatusBarHidden = viewController
        setNeedsStatusBarAppearanceUpdate()
    }

    @objc func setChildViewControllerForStatusBarStyle(_ viewController: UIViewController) {
        _childViewControllerForStatusBarStyle = viewController
        setNeedsStatusBarAppearanceUpdate()
    }

    open override var childForStatusBarHidden: UIViewController? {
        return _childViewControllerForStatusBarHidden
    }

    open override var childForStatusBarStyle: UIViewController? {
        return _childViewControllerForStatusBarStyle
    }

    open override var preferredStatusBarStyle: UIStatusBarStyle {
        return statusBarStyle
    }
}

public class AstroViewControllerBase: StatusBarStateViewController {
    @objc var launchDeepLinkUri: URL?
    @objc var isLaunchingWithDeepLink = false

    @objc public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?, launchDeepLinkUri launchDeepLinkUriOrNil: URL?) {
        launchDeepLinkUri = launchDeepLinkUriOrNil
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }

    @available(*, unavailable, message: "Use init(nibName:bundle:launchDeepLinkUri:)")
    public required init?(coder aDecoder: NSCoder) {
        fatalError("You cannot call this initializer.")
    }

    @available(*, unavailable, message: "Use init(nibName:bundle:launchDeepLinkUri:)")
    public required init() {
        fatalError("You cannot call this initializer.")
    }
}

public class AstroViewController: AstroViewControllerBase {
    @objc static let mainBundle = _mainBundle()

    var messageBus = MessageBus()
    @objc var worker: AstroWorker
    @objc var localization: LocalizationWrapper
    @objc var settingsStore: SettingsStore
    @objc var application: AstroApplication!
    @objc var pluginManager: PluginManager!

    @objc let cordovaViewController = CDVViewController()
    @objc let workerContainerView = AstroWorkerView()
    @objc let launchScreen = LaunchScreenViewController()

    public init(appJSURL: URL, launchOptions: [AnyHashable: Any]?, pluginRegistrations: ((PluginRegistrar) -> Void)? = nil) {
        var _ = cordovaViewController.view // Touch view to cause CDVViewController to initialize it's view hierarchy.

        guard let webView = cordovaViewController.webView as? UIWebView else {
            fatalError("Cordova's `webView` property returned a non UIWebView object which is unsupported!")
        }

        webView.injectNativeEnvironment()
        webView.alpha = 0

        let adaptor = UIWebViewAdaptor(webView: webView)
        worker = AstroWorker(appJSURL: appJSURL, messageBus: messageBus, webBridge: adaptor)

        localization = LocalizationWrapper(messageBus: messageBus)
        settingsStore = SettingsStore(messageBus: messageBus)

        super.init(nibName: nil, bundle: nil, launchDeepLinkUri: AstroViewController.deepLinkFromLaunchOptions(launchOptions))

        isLaunchingWithDeepLink = AstroViewController.hasUniversalDeepLink(launchOptions)

        view.backgroundColor = UIColor.white

        pluginManager = PluginManager(messageBus: messageBus, viewController: self)
        pluginManager.registerPlugin(name: "AlertViewPlugin", type: AlertViewPlugin.self)
        pluginManager.registerPlugin(name: "AnchoredLayoutPlugin", type: AnchoredLayoutPlugin.self)
        pluginManager.registerPlugin(name: "CounterBadgePlugin", type: CounterBadgePlugin.self)
        pluginManager.registerPlugin(name: "DefaultLoaderPlugin", type: DefaultLoaderPlugin.self)
        pluginManager.registerPlugin(name: "DrawerPlugin", type: DrawerPlugin.self)
        pluginManager.registerPlugin(name: "HeaderBarPlugin", type: HeaderBarPlugin.self)
        pluginManager.registerPlugin(name: "ImageViewPlugin", type: ImageViewPlugin.self)
        pluginManager.registerPlugin(name: "ListSelectPlugin", type: ListSelectPlugin.self)
        pluginManager.registerPlugin(name: "ModalViewPlugin", type: ModalViewPlugin.self)
        pluginManager.registerPlugin(name: "NavigationPlugin", type: NavigationPlugin.self)
        pluginManager.registerPlugin(name: "PromptViewPlugin", type: PromptViewPlugin.self)
        pluginManager.registerPlugin(name: "SearchBarPlugin", type: SearchBarPlugin.self)
        pluginManager.registerPlugin(name: "SecureStorePlugin", type: SecureStorePlugin.self)
        pluginManager.registerPlugin(name: "SharingPlugin", type: SharingPlugin.self)
        pluginManager.registerPlugin(name: "TabBarPlugin", type: TabBarPlugin.self)
        pluginManager.registerPlugin(name: "WebViewPlugin", type: WebViewPlugin.self)
        pluginManager.registerPlugin(name: "SegmentedPlugin", type: SegmentedPlugin.self)

        pluginRegistrations?(pluginManager)

        application = AstroApplication(viewController: self, messageBus: messageBus, pluginResolver: pluginManager)

        workerContainerView.translatesAutoresizingMaskIntoConstraints = false
        workerContainerView.isHidden = true
        view.addSubview(workerContainerView)
        workerContainerView.pinToSuperviewEdges()

        addChild(launchScreen)
        launchScreen.didMove(toParent: self)
        launchScreen.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(launchScreen.view)
        launchScreen.view.pinToSuperviewEdges()

        AstroWebUtils.addAstroUserAgent()

        NotificationCenter.default.addObserver(self,
                                               selector: #selector(appActivated),
                                               name: UIApplication.didBecomeActiveNotification,
                                               object: nil)
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(appDeactivated),
                                               name: UIApplication.willResignActiveNotification,
                                               object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    @objc static func deepLinkFromLaunchOptions(_ launchOptions: [AnyHashable: Any]?) -> URL? {
        if let options = launchOptions {
            // When launching the application via a push notification, the target URI can be found within the RemoteNotification payload.
            if let remoteNotification = options[UIApplication.LaunchOptionsKey.remoteNotification] as? NSDictionary,
                let remoteNotificationPayload = remoteNotification["payload"] as? NSDictionary,
                let remoteNotificationPayloadUrl = remoteNotificationPayload["url"] as? String,
                let deepLinkUri = URL(string: remoteNotificationPayloadUrl) {
                return stripDeepLinkScheme(deepLinkUri)
            }

            // The target URI may also be provided as an individual key within the launch options.
            if let deepLinkUri = options[UIApplication.LaunchOptionsKey.url] as? URL {
                return stripDeepLinkScheme(deepLinkUri)
            }
        }
        return nil
    }

    private static func stripDeepLinkScheme(_ initialUri: URL) -> URL? {
        var currentUri = initialUri.absoluteString

        // Note: ios provides the ability to define multiple url types and multiple url schemes within those types.
        if let infoPlist = Bundle.main.infoDictionary,
            let urlTypesArray = infoPlist["CFBundleURLTypes"] as? [[String: AnyObject]] {
                for urlType in urlTypesArray {
                    if let urlSchemes = urlType["CFBundleURLSchemes"] as? [String] {
                        for urlScheme in urlSchemes {
                            if let range = currentUri.range(of: "\(urlScheme)://") {
                                currentUri.removeSubrange(range)
                            }
                        }
                    }
                }
        }

        // Remove leading '/' before http:// that is necessary when passing in a url via deep link (ex. "scaffold:///http://www.google.com")
        // iOS likes to remove the ':' from the "http" if you have only two slashes after your custom url handle ("scaffold"). The solution
        // is to add the third '/' if passing a link through and then strip it away.
        if currentUri.hasPrefix("/http") {
            currentUri = currentUri.substring(from: currentUri.index(currentUri.startIndex, offsetBy: 1))
        }

        return URL(string: currentUri)
    }

    @objc public func receivedDeeplink(_ uri: URL) {
        if let uriString = AstroViewController.stripDeepLinkScheme(uri)?.absoluteString {
            let params = ["uri": uriString]
            application.trigger("private:receivedDeepLink", params: params)
        }
    }

    @objc public func appActivated() {
        application.trigger("appActivated")
    }

    @objc public func appDeactivated() {
        application.trigger("appDeactivated")
    }

    // Call from application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool in AppDelegate
    @objc public func continueUserActivity(_ userActivity: NSUserActivity) -> Bool {
        if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
            if let url = userActivity.webpageURL {
                if isLaunchingWithDeepLink {
                    launchDeepLinkUri = url
                    isLaunchingWithDeepLink = false
                } else {
                    receivedDeeplink(url)
                }
                return true
            }
        }
        return false
    }

    private static func hasUniversalDeepLink(_ launchOptions: [AnyHashable: Any]?) -> Bool {
        if let launchOptions = launchOptions,
            let userActivityInfo = launchOptions[UIApplication.LaunchOptionsKey.userActivityDictionary] as? NSDictionary,
            let activityType = userActivityInfo[UIApplication.LaunchOptionsKey.userActivityType] as? String,
            activityType == NSUserActivityTypeBrowsingWeb {
                return true
        }

        return false
    }

    @objc public func hideViewInWorkerView(_ view: UIView) {
        view.translatesAutoresizingMaskIntoConstraints = true
        workerContainerView.addSubview(view)
    }

    @objc public func dismissLaunchScreen() {
        launchScreen.willMove(toParent: nil)
        launchScreen.removeFromParent()
        launchScreen.view.removeFromSuperview()
    }

    public override func viewDidLoad() {
        super.viewDidLoad()

        // Add the worker's view for debugging purposes. A UIWebView must be
        // part of the view hierarchy to be remotely inspectable by Safari.
        addChild(cordovaViewController)
        cordovaViewController.didMove(toParent: self)
        cordovaViewController.webView.scrollView.scrollsToTop = false
        workerContainerView.addSubview(cordovaViewController.view)
        cordovaViewController.view.translatesAutoresizingMaskIntoConstraints = false
        cordovaViewController.view.pinToSuperviewEdges()
    }

    public override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        // Start the worker. Note that this is safe to call multiple times
        // as the worker ignores subsequent calls once it's booted.
        worker.boot()
    }
}
