import Foundation
import WebKit

protocol AttachedHeaderContent {
    var headerContent: HeaderContent? { get set }
}

class HeaderContentAttachment: UIView {
    var headerContent: HeaderContent?
}

class PlaceholderView: UIView { }

class PlaceholderViewController: UIViewController {

    fileprivate let snapshotView: UIView

    @objc init(placeholderView: UIView) {
        self.snapshotView = placeholderView
        super.init(nibName: nil, bundle: nil)

        placeholderView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(placeholderView)
        placeholderView.pinToSuperviewEdges()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init?(coder:) is not available")
    }

    override func loadView() {
        self.view = PlaceholderView(frame: UIScreen.main.bounds)
    }
}

extension UIViewController: AttachedHeaderContent {
    var headerContent: HeaderContent? {
        get {
            let attachment = navigationItem.rightBarButtonItem?.customView as? HeaderContentAttachment
            return attachment?.headerContent
        }
        set {
            let attachment = HeaderContentAttachment()
            attachment.headerContent = newValue
            navigationItem.rightBarButtonItem = UIBarButtonItem(customView: attachment)
        }
    }
}

open class NavigationPlugin: Plugin, ViewPlugin, PersistingNavigationControllerDelegate, ParentPlugin {

    @objc let typedViewController = PersistingNavigationController()
    @objc open var viewController: UIViewController {
        return typedViewController
    }

    private var loaderBackgroundColor = UIColor.white

    private var pluginAddressStack = [MessageAddress]()

    private var pwaDidRender: (() -> Void)?
    private var pwaDidRenderBackupRemoval: DispatchWorkItem?

    public required init(address: MessageAddress, messageBus: MessageBus, pluginResolver: PluginResolver, options: JSONObject?) {
        super.init(address: address, messageBus: messageBus, pluginResolver: pluginResolver, options: options)

        typedViewController.persistingDelegate = self

        // using `navigationBar.hidden` rather then setting `navigationBarHidden` here
        // because setting `navigationBarHidden` to true also breaks the swipe gesture
        // for going back in a navigation controller.
        typedViewController.navigationBar.isHidden = true

        addRpcMethodShim("navigateToPlugin") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let address: MessageAddress = MethodShimUtils.getArg(params, key: "address", respond: respond) {
                if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
                    self.navigateToPlugin(address, options: options, respond: respond)
                }
            }
            /////////////////////////////////////////////////////////////
        }

        addRpcMethodShim("setHeaderBar") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let address: MessageAddress = MethodShimUtils.getArg(params, key: "address", respond: respond) {
                self.setHeaderBar(address, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        addRpcMethodShim("back") { _, respond in
            ////////// This will be autogenerated at some point //////////
            self.back(respond)
            /////////////////////////////////////////////////////////////
        }

        addRpcMethodShim("canGoBack") { _, respond in
            ////////// This will be autogenerated at some point //////////
            self.canGoBack(respond)
            /////////////////////////////////////////////////////////////
        }

        addRpcMethodShim("popToRoot") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let options: JSONObject = MethodShimUtils.getArg(params, key: "options", respond: respond) {
                if let animated: Bool = options.get("animated", respond: respond) {
                    self.popToRoot(animated: animated, respond: respond)
                }
            }
            /////////////////////////////////////////////////////////////
        }

        addRpcMethodShim("enableBackGesture") { _, respond in
            ////////// This will be autogenerated at some point //////////
            self.enableBackGesture(respond)
            /////////////////////////////////////////////////////////////
        }

        addRpcMethodShim("disableBackGesture") { _, respond in
            ////////// This will be autogenerated at some point //////////
            self.disableBackGesture(respond)
            /////////////////////////////////////////////////////////////
        }

        addRpcMethodShim("getTopPluginAddress") { _, respond in
            guard let address = self.pluginAddressStack.last else {
                respond(.result(NSNull()))
                return
            }
            return respond(.result(address))
        }

        addRpcMethodShim("setLoaderViewBackgroundColor") { params, respond in
            if let options: JSONObject = MethodShimUtils.getArg(params, key: "options", respond: respond) {
                if let colorHex: String = options.get("color", respond: respond) {
                    guard let color = UIColor(hex: colorHex) else {
                        respond(.error("Invalid hex value: \(colorHex)"))
                        return
                    }
                    self.loaderBackgroundColor = color
                }
            }
        }
    }

    // MARK: - RPC Methods

    // @RpcMethod
    func navigateToPlugin(_ address: MessageAddress, options: JSONObject?, respond: RPCMethodCallback) {
        if let headerContent = headerContentFromOptions(options, respond: respond) {
            if let viewPlugin: ViewPlugin = pluginResolver.pluginInstanceByAddress(address, respond: respond) {
                let viewController = viewPlugin.viewController
                viewController.headerContent = headerContent
                navigateWithViewController(viewController, animated: animatedFromOptions(options, respond: respond), respond: respond)
                self.pluginAddressStack.append(address)
                if let webViewPlugin = viewPlugin as? WebViewPlugin {
                    webViewPlugin.parentPlugin = self
                }
            }
        }
    }

    // @RpcMethod
    func setHeaderBar(_ address: MessageAddress, respond: RPCMethodCallback) {
        if let headerBar: HeaderBarPlugin = pluginResolver.pluginInstanceByAddress(address, respond: respond) {
            typedViewController.headerContentCoordinator = headerBar
            headerBar.navigationPlugin = self
        }
    }

    // @RpcMethod
    func back(_ respond: RPCMethodCallback) {
        let _ = typedViewController.popViewController(animated: true)
    }

    // @RpcMethod
    func canGoBack(_ respond: RPCMethodCallback) {
        respond(.result(canGoBack()))
    }

    // @RpcMethod
    func popToRoot(animated: Bool, respond: RPCMethodCallback) {
        if (self.pluginAddressStack.count == 0) {
            respond(.result(NSNull()))
            return
        }

        let firstAddress = self.pluginAddressStack.removeFirst()
        self.pluginAddressStack.removeAll()
        self.pluginAddressStack.append(firstAddress)
        typedViewController.popToRoot(animated: animated)
    }

    // @RpcMethod
    func enableBackGesture(_ respond: RPCMethodCallback) {
        typedViewController.interactivePopGestureRecognizer?.isEnabled = true
    }

    // @RpcMethod
    func disableBackGesture(_ respond: RPCMethodCallback) {
        typedViewController.interactivePopGestureRecognizer?.isEnabled = false
    }

    private func callPwaDidRender() {
        if let pwaDidRender = pwaDidRender {
            pwaDidRender()
        }
        pwaDidRender = nil
    }

    private func displaySnapshot(placeholder: PlaceholderViewController, source: AstroWebViewController) {
        // When we navigate back, there is a flash of old content while
        // the webview loads the previous page. To fix that, we grab the placeholder that is being
        // dismissed and plaster it's snapshotView over the newly presented view.
        // We remove the snapshot view when we receive a pwaRendered event from the site. If we have not
        // received this event in 1 second then we remove the snapshot view.

        let placeholderView = placeholder.snapshotView
        placeholderView.removeFromSuperview()
        source.view.addSubview(placeholderView)
        source.view.bringSubviewToFront(placeholderView)
        placeholderView.pinToSuperviewEdges()

        self.pwaDidRender = {
            source.finishBackNavigation()
            UIView.animate(withDuration: 0.2,
                           animations: {placeholderView.alpha = 0.0},
                           completion: { _ in
                            placeholderView.removeFromSuperview()
            })

        }
        self.pwaDidRenderBackupRemoval = DispatchWorkItem { self.callPwaDidRender()}
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: self.pwaDidRenderBackupRemoval!)
    }

    func response(rpcMethodResult: RPCMethodResult) {
        // snapshotTopPlugin is not called by App.js
        // no need to respond back over JS-native bridge...which would normally be what the
        // response obejct is used for...thus no need to do anything here
    }

    func snapshotTopPlugin() {
        if let topPlugin = self.pluginAddressStack.last, let topWebViewPlugin: WebViewPlugin = pluginResolver.pluginInstanceByAddress(topPlugin, respond: response) {
            topWebViewPlugin.typedViewController?.takeSnapshot()
        }
    }

    // MARK: - PersistingNavigationControllerDelegate

    @objc open func didPushToViewController(_ viewController: UIViewController) {
    }

    @objc open func didPopToViewController(_ viewController: UIViewController) {
        triggerBack()
    }

    @objc func pop(from source: UIViewController?, to destination: UIViewController?) {
        guard let placeholder = destination as? PlaceholderViewController else {
            //If the previous controller is not a placeholder means we
            //should pop the address stack because we've come from a different plugin
            self.pluginAddressStack.removeLast()
            if let source = source as? AstroWebViewController {
                source.delegate?.astroWebViewControllerOnPluginRemoved(source)
            }
            return
        }

        guard let source = source as? AstroWebViewController else {
            return
        }

        source.navigateBack {
            self.displaySnapshot(placeholder: placeholder, source: source)

            var newStack = self.typedViewController.viewControllers.dropLast()
            newStack.append(source)
            self.typedViewController.viewControllers = Array(newStack)
        }
    }

    @objc func popToRoot(poppedControllers: [UIViewController]?, root: UIViewController?) {
        //Check if current root view controller is not a Placeholder, then return
        guard let placeholder = root as? PlaceholderViewController else {
            return
        }

        guard let poppedControllers = poppedControllers else {
            return
        }

        for (index, viewController) in poppedControllers.enumerated() {
            //Check if viewController is a WebView
            guard let source = viewController as? AstroWebViewController else {
                continue
            }

            source.navigateToEntry(entryIndex: -index-1) {
                self.displaySnapshot(placeholder: placeholder, source: source)

                self.typedViewController.viewControllers = [viewController]
            }

            break
        }
    }

    @objc open func didPopToRootViewController(_ viewController: UIViewController) {
        triggerPopToRoot()
    }

    // MARK: - ParentPlugin

    func removeChildPlugin(_ child: ViewPlugin) {
        typedViewController.removeViewController(child.viewController)
    }

    func pwaNavigate(_ webViewPlugin: WebViewPlugin, params: JSONObject, respond: RPCMethodCallback) {
        typedViewController.inPwaNavigation = true

        if !params.isEmpty, let headerContent = headerContentFromOptions(params, respond: respond) {
            typedViewController.pwaHeaderContent = headerContent
        }

        if let snapshotView = webViewPlugin.latestSnapshot() {
            let placeholder = PlaceholderViewController(placeholderView: snapshotView)

            // splice in the placeholder where the web view plugin is currently
            // and (re)push it as a new navigation
            typedViewController.viewControllers[typedViewController.viewControllers.count - 1] = placeholder

            respond(.result(NSNull()))

            typedViewController.pushViewController(webViewPlugin.viewController, animated: true)
            let loaderView = UIView(frame: webViewPlugin.viewController.view.bounds)
            loaderView.backgroundColor = self.loaderBackgroundColor
            webViewPlugin.viewController.view.addSubview(loaderView)
            webViewPlugin.viewController.view.bringSubviewToFront(loaderView)
            loaderView.pinToSuperviewEdges()
            pwaDidRender = {loaderView.removeFromSuperview()}

            pwaDidRenderBackupRemoval = DispatchWorkItem { self.callPwaDidRender()}
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: self.pwaDidRenderBackupRemoval!)
        }
    }

    func pwaRendered(respond: RPCMethodCallback) {
        pwaDidRenderBackupRemoval?.cancel()
        pwaDidRenderBackupRemoval = nil
        callPwaDidRender()
        respond(.result(NSNull()))
    }

    // MARK: - Internal helper methods

    private func navigateWithViewController(_ viewController: UIViewController, animated: Bool, respond: RPCMethodCallback) {
        typedViewController.pushViewController(viewController, animated: animated)
    }

    private func currentUrl() -> String {
        if let webViewController = typedViewController.topViewController as? AstroWebViewController {
            return webViewController.currentURL.absoluteString
        } else if let viewController = typedViewController.topViewController {
            return "\(viewController)"
        } else {
            fatalError("currentUrl called but navigation stack is empty!")
        }
    }

    private func canGoBack() -> Bool {
        return typedViewController.viewControllers.count > 1
    }

    private func triggerBack() {
        let params: JSONObject = [
            "url": currentUrl(),
            "canGoBack": canGoBack()
        ]
        trigger("back", params: params)
    }

    private func triggerPopToRoot() {
        let params: JSONObject = [
            "url": currentUrl()
        ]

        trigger("popToRoot", params: params)
    }

    private func headerContentFromOptions(_ options: JSONObject?, respond: RPCMethodCallback) -> HeaderContent? {
        let options = options ?? JSONObject()
        var headerContent: HeaderContent?

        if options.keys.contains("header") {
            if let headerJSON: JSONObject = options.get("header", respond: respond) {
                headerContent = HeaderContent(jsonObject: headerJSON, pluginResolver: pluginResolver, respond: respond)
            }
        } else {
            headerContent = HeaderContent()
        }

        return headerContent
    }

    private func animatedFromOptions(_ options: JSONObject?, respond: RPCMethodCallback) -> Bool {
        let options = options ?? JSONObject()

        if options.keys.contains("animated") {
            return options.get("animated", respond: respond)!
        }

        // If the value isn't set, then we default to animation being enabled.
        return true
    }
}
