//
//  AstroWebViewController.swift
//  Astro
//
//  Created by Jeremy Wiebe on 2015-06-04.
//  Renamed by Jason Voll on 2015-09-11 (UIWebViewController --> AstroWebViewController)
//  Copyright (c) 2015 Mobify Research & Development Inc. All rights reserved.
//

import Foundation
import WebKit

private var AstroWebViewControllerContext = 0

protocol AstroWebViewControllerDelegate: class {
    func astroWebViewController(_ webViewController: AstroWebViewController, navigateWithRequest request: URLRequest, showLoader: Bool)
    func astroWebViewControllerOnPluginRemoved(_ webViewController: AstroWebViewController)
    func astroWebViewController(_ webViewController: AstroWebViewController, updateWebView webView: UIView)
    func astroWebViewControllerClearWebView()
    func astroWebViewControllerDidNavigateBack(_ controller: AstroWebViewController)
}

class AstroWebViewController: UIViewController, Persistable, ManagedContentInsets, ManagedContentOffsets, ManagedContentViewState, UIGestureRecognizerDelegate {
    private let 😫 = "contentInset"
    private let 😪 = "scrollIndicatorInsets"

    weak var delegate: AstroWebViewControllerDelegate?

    @objc var webView: WKWebView? {
        willSet {
            removeScrollViewInsetObserver()
        }
        didSet {
            addScrollViewInsetObserver()
        }
    }

    @objc var currentURL: URL {
        guard let url = webView?.url else {
            return URL(string: "about:blank")!
        }
        return url
    }

    @objc private(set) var encodedState: Data?
    private var allowNextInsetSet: Bool = false
    private var contentExtendsToBottomOfFrame: Bool = false
    @objc var lastKnownURL: URL?
    private var webViewConfiguration: WKWebViewConfiguration
    @objc var snapshot: UIView?
    private var gr: UIGestureRecognizer?

    @objc init(state: Data? = nil, webViewConfiguration: WKWebViewConfiguration) {
        self.webViewConfiguration = webViewConfiguration

        super.init(nibName: nil, bundle: nil)

        initWebView()

        if #available(iOS 11.0, *) {
            getScrollViewFromWebView()?.contentInsetAdjustmentBehavior = .never
        } else {
            automaticallyAdjustsScrollViewInsets = false
        }

        if let state = state {
            webView!.decodeState(state)
        }

        // We expect that the view controller hosting this AstroWebViewController will provide
        // a background color.  This simplifies stacking in the WebViewPlugin as we don't
        // have to apply the plugin's background color to each new webview in the stack
        webView!.backgroundColor = UIColor.clear

        addWebView()

        // Sets the deceleration rate of UIWebView to the normal deceleration rate of a scroll view
        getScrollViewFromWebView()?.decelerationRate = UIScrollView.DecelerationRate.normal

        hideViewInWorkerView()

        self.gr = UITapGestureRecognizer(target: self, action: #selector(didTap(gestureRecognizer:)))
        self.gr?.delegate = self
        webView?.scrollView.addGestureRecognizer(self.gr!)
    }

    func takeSnapshot() {
        self.snapshot = self.webView?.snapshotView(afterScreenUpdates: false)
    }

    @objc func didTap(gestureRecognizer: UIGestureRecognizer) {
        takeSnapshot()
    }

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("initWithCoder: is not supported by this class")
    }

    deinit {
        removeScrollViewInsetObserver()
        webView?.removeFromSuperview()
    }

    fileprivate func initWebView() {
        let wkWebView = WKWebView(frame: CGRect.zero, configuration: webViewConfiguration)
        wkWebView.injectNativeEnvironment()
        webView = wkWebView
    }

    private func addWebView() {
        if let webView = webView, webView.superview != view {
            webView.removeFromSuperview()
            webView.removeConstraints(webView.constraints)
            webView.frame = view.bounds // Set this now despite constraints that will do the same below, to solve a first-display-popping issue
            webView.translatesAutoresizingMaskIntoConstraints = false
            view.insertSubview(webView, at: 0)

            // The webview will will take up the full screen if it is the outermost view.
            // A parent plugin will need to constrain it inside the safe area if necessary.
            // For example: NavigationPlugin constrains it's children inside the safe area
            webView.pinToSuperviewEdges()

        }
    }

    private func hideViewInWorkerView() {
        if let webView = webView,
           let astroViewController = UIApplication.shared.keyWindow?.rootViewController as? AstroViewController {
               webView.removeFromSuperview()
               astroViewController.hideViewInWorkerView(webView)
        }
    }

    @objc func didGetAddedToContentView() {
        addWebView()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // This is required due to some strange behaviour with WKWebView - despite addWebView() doing the same thing,
        // WKWebViews will have already been added by addToContentView prior to this function. So we need to do this to
        // address some first-display-popping issues as well as the 'WKWebView doesn't scroll to middle of page on load' issue.
        // See https://openradar.appspot.com/22855188
        webView?.frame = view.bounds
        addWebView()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        hideViewInWorkerView()
    }

    private func getScrollViewFromWebView() -> UIScrollView? {
        return webView?.scrollView
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
        guard context == &AstroWebViewControllerContext else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
            return
        }

        // This reverts changes to `webView.scrollView.contentInset` and `webView.scrollView.scrollIndicatorInsets` if `allowNextInsetSet` is false.
        // We do this because the web view sometimes tries to manage its own insets (when the keyboard is shown for example) while we want to retain
        // their complete control.
        if allowNextInsetSet {
            allowNextInsetSet = false
            return
        }

        if let oldValue = change?[NSKeyValueChangeKey.oldKey] as? NSValue {
            let originalInsets = oldValue.uiEdgeInsetsValue

            allowNextInsetSet = true

            if let scrollView = getScrollViewFromWebView() {
                if keyPath == 😫 {
                    updateContentInsets(originalInsets, withScrollView: scrollView)
                } else if keyPath == 😪 {
                    scrollView.scrollIndicatorInsets = originalInsets
                }
            }

            allowNextInsetSet = false
        }
    }

    @objc func addScrollViewInsetObserver() {
        if let scrollView = getScrollViewFromWebView() {
            scrollView.addObserver(self, forKeyPath: 😫, options: .old, context: &AstroWebViewControllerContext)
            scrollView.addObserver(self, forKeyPath: 😪, options: .old, context: &AstroWebViewControllerContext)
        }
    }

    @objc func removeScrollViewInsetObserver() {
        if let scrollView = getScrollViewFromWebView() {
            scrollView.removeObserver(self, forKeyPath: 😫)
            scrollView.removeObserver(self, forKeyPath: 😪)
        }
    }

    @objc func updateContentInsets(_ insets: UIEdgeInsets, withScrollView scrollView: UIScrollView) {
        // calculateOffsetBasedOnInsets() needs to be called before setting contentInset and contentOffset
        let newContentOffset = calculateOffsetBasedOnInsets(insets: insets, scrollView: scrollView)
        scrollView.contentInset = insets
        scrollView.setContentOffset(newContentOffset, animated: false)
    }

    func calculateOffsetBasedOnInsets(insets: UIEdgeInsets, scrollView: UIScrollView) -> CGPoint {
        let existingOffset = scrollView.contentOffset
        let existingInset = scrollView.contentInset

        var offset = CGPoint(x: existingOffset.x, y: (existingInset.top - insets.top) + existingOffset.y)

        if contentExtendsToBottomOfFrame {
            if (scrollView.contentSize.height <= scrollView.frame.height) {
                offset = CGPoint(x: existingOffset.x, y: -insets.top)
            } else if (scrollView.contentSize.height - existingOffset.y < scrollView.frame.height) {
                let yOffset = scrollView.contentSize.height - scrollView.frame.height
                offset = CGPoint(x: existingOffset.x, y: yOffset)
            }
            contentExtendsToBottomOfFrame = false
        }

        return offset
    }

    func adjust(contentInsets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets) {
        if let scrollView = getScrollViewFromWebView() {
            allowNextInsetSet = true
            updateContentInsets(contentInsets, withScrollView: scrollView)
            allowNextInsetSet = true
            scrollView.scrollIndicatorInsets = scrollIndicatorInsets
        }
    }

    func setContentExtendsToBottomOfFrame() {
        contentExtendsToBottomOfFrame = true
    }

    @objc func save() {
        saveLastKnownUrl()

        // we should only encode state on webviews that we don't want the rehydration logic to try and rehydrate
        // (we are using encodedState != nil as a flag for trying to rehydrate
        if let webView = webView {
            let stateData = webView.encodeState()

            let stateSizeInKb = Double(stateData.count) / 1024.0
            AstroLog.logger(AstroLog.Application).debug("*** Saved state: \(stateSizeInKb) kb. URL: \(lastKnownURL?.absoluteString ?? "No URL")")
            encodedState = stateData
        } else {
            AstroLog.logger(AstroLog.Application).error("*** Called save() but webView is nil!!!!")
        }

        webView?.removeFromSuperview()
        webView = nil
        delegate?.astroWebViewControllerClearWebView()
    }

    @objc func saveLastKnownUrl() {
        if let url = webView?.url {
            lastKnownURL = url
        } else {
            AstroLog.logger(AstroLog.Application).error("*** Called saveLastKnownUrl() but webView is nil!!!!")
        }
    }

   func restore(contentInsets: UIEdgeInsets?, scrollIndicatorInsets: UIEdgeInsets?) {
        initWebView()

        if let webView = webView {
            delegate?.astroWebViewController(self, updateWebView: webView)
            webView.translatesAutoresizingMaskIntoConstraints = false
            getScrollViewFromWebView()?.decelerationRate = UIScrollView.DecelerationRate.normal
            view.addSubview(webView)
            webView.pinToSuperviewEdges()
            if let contentInsets = contentInsets {
                if let scrollIndicatorInsets = scrollIndicatorInsets {
                    adjust(contentInsets: contentInsets, scrollIndicatorInsets: scrollIndicatorInsets)
                } else {
                    adjust(contentInsets: contentInsets, scrollIndicatorInsets: contentInsets)
                }
            }

            if let state = self.encodedState {
                webView.decodeState(state)
                reloadFromLastKnownURL()
                encodedState = nil
            }

            webView.frame = view.bounds
        }
    }

    // Should only be used with WKWebView
    @objc func reloadFromLastKnownURL(_ showLoader: Bool = true) {
        if let url = lastKnownURL {
            let req = URLRequest(url:url)
            AstroLog.logger(AstroLog.WebAdaptor).debug("reloading lastKnownUrl: \(url)")
            delegate?.astroWebViewController(self, navigateWithRequest: req, showLoader: showLoader)
        }
    }

    @objc func reloadWebViewIfNeeded() {
        // The WKWebView has a bug where it randomly goes blank when pushed onto a UINavigationController
        // We can check for this by confirming that the URL property is nil. If one of these has gone nil, 
        // we attempt to reload it. Finally, we only take action if we haven't been persisted.
        if encodedState == nil,
           webView?.url == nil,
           webView?.isLoading == false,
           let url = lastKnownURL {
                AstroLog.logger(AstroLog.WebAdaptor).debug(
                    "Found AstroWebViewController with nil url. " +
                    "Attempting restore to last known url: \(url).")
                reloadFromLastKnownURL(false)
        }
    }

    @objc func navigateBack(_ complete: @escaping () -> Void) {
        self.webView?.evaluateJavaScript("if (window.Progressive && window.Progressive.api && window.Progressive.api.navigateBack) {window.Progressive.api.navigateBack();} else {window.history.back();}") { _, _ in
            complete()
        }
    }

    func navigateToEntry(entryIndex: Int, _ complete: @escaping () -> Void) {
        self.webView?.evaluateJavaScript("window.history.go(\(entryIndex));") { _, _ in
            complete()
        }
    }

    @objc func finishBackNavigation() {
        self.delegate?.astroWebViewControllerDidNavigateBack(self)
    }
}
