//
//  WKWebViewAdaptor.swift
//  Astro
//
//  Created by Jason Voll on 2015-07-21.
//  Copyright (c) 2015 Mobify Research & Development Inc. All rights reserved.
//

import Foundation
import UIKit
import WebKit

class WKWebViewAdaptor: AstroWebViewAdaptor, WKUIDelegate, WKNavigationDelegate {
    private static let BridgeUrlScheme = "astro"

    // This can be `nil` after first creating the adaptor _and_ in cases where
    // the WebViewPlugin is stacking (NavigationPlugin) and we've navigated
    // to (or back to) a native plugin.
    private var webView: WKWebView? {
        didSet {
            setupScrollView()
        }
    }

    @objc var chainedWebViewDelegate: WKNavigationDelegate? // swiftlint:disable:this weak_delegate

    override var canGoBack: Bool {
        if let webView = webView {
            return webView.canGoBack
        }
        return false
    }

    override var currentURL: URL? {
        return webView?.url
    }

    @objc init(webView: WKWebView) {
        super.init()
        registerWebView(webView)
    }

    deinit {
        // The webView's scrollView delegate has been set to a WKWebViewAdaptor
        // We set the delegate to nil so ARC won't try to release the reference twice.
        // https://forums.developer.apple.com/thread/19027
        self.webView?.scrollView.delegate = nil
    }

    override func setupNavigationTypeWrapper() {
        navigationTypes = [
            .linkActivated: WKNavigationType.linkActivated.rawValue,
            .formSubmitted: WKNavigationType.formSubmitted.rawValue,
            .backForward: WKNavigationType.backForward.rawValue,
            .reload: WKNavigationType.reload.rawValue,
            .formResubmitted: WKNavigationType.formResubmitted.rawValue,
            .other: WKNavigationType.other.rawValue
        ]
    }

    override func registerWebView(_ webView: UIView) {
        guard let webView = webView as? WKWebView else {
            fatalError("Passed a non-WKWebView to registerWebView")
        }

        // Restore old web view to its original state before we release it
        self.webView?.navigationDelegate = chainedWebViewDelegate
        self.chainedWebViewDelegate = nil
        self.webView = nil

        // Hook up the new webview if one has been provided
        self.webView = webView
        self.chainedWebViewDelegate = webView.navigationDelegate
        webView.uiDelegate = self
        webView.navigationDelegate = self

        // WKWebView uses a slightly different view hierarchy from UIWebView
        // and so we need to make the UIScrollView of it clear so that
        // the setBackgroundColor on WebViewPlugin works properly.
        webView.scrollView.backgroundColor = UIColor.clear

        // Allows the web view adaptor to cancel auto-scroll if scrolling is disabled
        self.webView?.scrollView.delegate = self
    }

    override func unregisterWebView() {
        webView = nil
        chainedWebViewDelegate = nil
    }

    override func stopLoading() {
        webView?.stopLoading()
    }

    override func goBack() {
        let _ = webView?.goBack()
    }

    override func reload() {
        let _ = webView?.reload()
    }

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

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        // Sets the deceleration rate of WKWebView to the normal deceleration rate of a scroll view.
        // Must be set in the `scrollViewWillBeginDragging` delegate method for WKWebView.
        // See http://stackoverflow.com/questions/31369538/cannot-change-wkwebviews-scroll-rate-on-ios-9-beta
        scrollView.decelerationRate = UIScrollView.DecelerationRate.normal
    }

    // Returns the cookie value for cookie name if the cookie exists. Nil if the cookie doesn't exist.
    override func getCookie(named cookieName: String, completionHandler: @escaping (_ cookieValue: String?, _ error: String?) -> Void) {
        guard let webView = webView else {
            return completionHandler(nil, "Could not fetch cookie from nil webView")
        }

        let cookieFetchJs = "document.cookie"
        webView.evaluateJavaScript(cookieFetchJs) { cookie, error in
            guard error == nil else {
                completionHandler(nil, "No cookie named \(cookieName). Error: \(error.debugDescription)")
                return
            }
            guard let cookie = cookie as? String else {
                completionHandler(nil, nil)
                return
            }
            guard let cookieValue = AstroWebViewAdaptor.getCookieValueFromString(cookieName, cookie: cookie) else {
                completionHandler(nil, nil)
                return
            }
            completionHandler(cookieValue, nil)
        }
    }

    override func deleteCookie(named cookieName: String, completionHandler: @escaping () -> Void) {
        guard let webView = webView else {
            return
        }

        let cookieDeleteJs = "document.cookie = \"\(cookieName)=; expires=Thu, 01 Jan 1970 00:00:01 GMT;\";"
        webView.evaluateJavaScript(cookieDeleteJs) { _, _ in
            completionHandler()
        }
    }

    private func guardWebView() {
        if webView == nil {
            preconditionFailure("Somehow attempted to send a message to a web view without first calling registerWebView")
        }
    }

    override func load(_ request: URLRequest) {
        guardWebView()
        if let url = request.url {
            AstroLog.logger(AstroLog.WebAdaptor).info("Loading url: \(url)")
            nextNavigationIsExplicit = true
            if url.isFileURL {
                // This will always be true since we don't allow WKWebView pre-iOS 9.0 however we need
                // this check for compilation since the loadFileURL API doesn't exist on iOS 8.x
                if #available(iOS 9.0, *) {
                    webView!.loadFileURL(url, allowingReadAccessTo: url)
                }
            } else {
                webView!.load(request)
            }
        }
    }

    override func sendMessage(to address: MessageAddress, data: String) {
        guard let webView = webView else {
            return
        }
        let receiveMessageJavascript = "window.Astro.receiveMessage('\(address)', \(data))"
        AstroLog.logger(AstroLog.Messaging).debug("---> Sending message to JavaScript: \(receiveMessageJavascript)")
        webView.evaluateJavaScript(receiveMessageJavascript, completionHandler: nil)
    }

    @objc func processBridgeMessages() {
        guardWebView()
        webView!.evaluateJavaScript("window.Astro.fetchMessages();", completionHandler: processBridgeMessageJson)
    }

    @objc func chainedDelegate_decidePolicyForNavigationAction(_ webView: WKWebView, navigationAction: WKNavigationAction,
        decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
            if let delegate = chainedWebViewDelegate {
                delegate.webView?(webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler)
            } else {
                decisionHandler(.allow)
            }
    }

    override func saveOffset() {
        guard let webview = self.webView else {
            return
        }
        self.contentOffsetStack.append(webview.scrollView.contentOffset)
    }

    override func didNavigateBack() {
        self.webView?.scrollView.contentOffset = self.contentOffsetStack.removeLast()
    }

    // MARK: - WKNavigationDelegate
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        let request = navigationAction.request
        let navigationType = navigationAction.navigationType
        let currentNavigationIsExplicit = nextNavigationIsExplicit

        guard let url = request.url else {
            AstroLog.logger(AstroLog.WebAdaptor).error("WKWebViewDelegate received request with nil url: \(request)")
            self.denyLoad("UIWebViewDelegate received request with nil url", url: nil, decisionHandler: decisionHandler)
            return
        }

        AstroLog.logger(AstroLog.WebAdaptor).debug("WKWebViewDelegate webView(_:decidePolicyForNavigationAction:decisionHandler)" +
            "\n    request.URL: \(url)" +
            "\n    request.HTTPMethod: \(request.httpMethod!)" +
            "\n    request.mainDocumentURL: \(request.mainDocumentURL!)" +
            "\n    WKNavigationType: \(navigationType.rawValue)" +
            "\n    isLoading: \(isLoading)" +
            "\n    currentNavigationIsExplicit: \(currentNavigationIsExplicit)" +
            "\n    nextNavigationIsExplicit: \(nextNavigationIsExplicit)")

        // Reset before returning
        nextNavigationIsExplicit = false

        // Astro bridge
        if url.scheme == WKWebViewAdaptor.BridgeUrlScheme {
            processBridgeMessages()
            return decisionHandler(.cancel)
        }

        chainedDelegate_decidePolicyForNavigationAction(webView, navigationAction: navigationAction, decisionHandler: {(policy: WKNavigationActionPolicy) in
            if policy == .cancel {
                return decisionHandler(.cancel)
            }

            if WKWebViewAdaptor.isITunesURL(url) || WKWebViewAdaptor.isDeepLinkSchemeURL(url) {
                // This is an app store link
                UIApplication.shared.openURL(url)
                self.webClientDelegate?.notifyAppStoreLinkOpened()
                self.denyLoad("iTunes/deep link URL -- deny so iOS can handle it", url: url, decisionHandler: decisionHandler)
                return
            }

            let _ = self.shouldAllowLoadForNavigationTypeHelper(navigationType.rawValue,
                currentNavigationIsExplicit: currentNavigationIsExplicit,
                url: url,
                request: request,
                decisionHandler: decisionHandler)
        })
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        AstroLog.logger(AstroLog.WebAdaptor).debug("WKNavigationDelegate webView(_:didFinishNavigation) to \(webView.url != nil ? webView.url!.absoluteString : "")")
        chainedWebViewDelegate?.webView?(webView, didFinish: navigation)

        // Don't notify the web client delegate when iframes finish loading.
        if isLoading {
            webClientDelegate?.pageDidFinishLoading()
        }

        lastLoadedUrl = webView.url
        isRestoring = false
        isLoading = false
    }

    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        AstroLog.logger(AstroLog.WebAdaptor).info("WKNavigationDelegate webView(_:didStartProvisionalNavigation:)")
        chainedWebViewDelegate?.webView?(webView, didStartProvisionalNavigation: navigation)
    }

    func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
        AstroLog.logger(AstroLog.WebAdaptor).debug("WKNavigationDelegate webView(_:didFailProvisionalNavigation:withError:)\n    error: \(error)")
        failedNavigationWithError(error as NSError)
        chainedWebViewDelegate?.webView?(webView, didFailProvisionalNavigation: navigation, withError: error)
    }

    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        failedNavigationWithError(error as NSError)
        chainedWebViewDelegate?.webView?(webView, didFail: navigation, withError: error)
    }

    // Note: due to a bug in iOS 8, this is only ever called in iOS 9/Xcode 7 and above
    func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
            AstroLog.logger(AstroLog.WebAdaptor).debug("WKNavigationDelegate webView(_:didReceiveAuthenticationChallenge:completionHandler:)")

            if AstroConfig.allowUntrustedHTTPSCertificate.boolValue,
                let serverTrust = challenge.protectionSpace.serverTrust {
                let cred = URLCredential(trust: serverTrust)
                completionHandler(.useCredential, cred)
            } else {
                completionHandler(.performDefaultHandling, nil)
            }
    }

    // Stubs for remainder of WKNavigationDelegate methods (useful for debugging)
    func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
        AstroLog.logger(AstroLog.WebAdaptor).debug("WKNavigationDelegate webView(_:didCommitNavigation:)")
    }

    func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
        AstroLog.logger(AstroLog.WebAdaptor).debug("WKNavigationDelegate webView(_:didReceiveServerRedirectForProvisionalNavigation:")
    }

    func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse,
        decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
            AstroLog.logger(AstroLog.WebAdaptor).debug("WKNavigationDelegate webView(_:decidePolicyForNavigationResponse:decisionHandler): -- allow by default")
            decisionHandler(.allow)
    }

    // MARK: - WKUIDelegate
    // presents native user interface elements on behalf of a webpage
    func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        AstroLog.logger(AstroLog.WebAdaptor).debug("WKUIDelegate runJavaScriptAlertPanelWithMessage")
        webClientDelegate?.showAlertPanelWithMessage(message, initiatedByFrame: frame, completionHandler: completionHandler)
    }

    func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
        AstroLog.logger(AstroLog.WebAdaptor).debug("WKUIDelegate runJavaScriptConfirmPanelWithMessage")
        webClientDelegate?.showConfirmPanelWithMessage(message, initiatedByFrame: frame, completionHandler: completionHandler)
    }

    func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
        AstroLog.logger(AstroLog.WebAdaptor).debug("WKUIDelegate runJavaScriptTextInputPanelWithPrompt")
        webClientDelegate?.showTextInputPanelWithPrompt(prompt, defaultText: defaultText, initiatedByFrame: frame, completionHandler: completionHandler)
    }
}
