//
//  UIWebViewAdaptor.swift
//  Astro
//
//  Created by Jeremy Wiebe on 2015-04-28.
//  Copyright (c) 2015 Mobify Research & Development Inc. All rights reserved.
//

import Foundation
import UIKit
import WebKit

class UIWebViewAdaptor: AstroWebViewAdaptor, UIWebViewDelegate {
    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: UIWebView? {
        didSet {
            setupScrollView()
        }
    }

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

    private var scriptsToAdd = [String]()

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

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

    // MARK: - Initializers

    @objc init(webView: UIWebView) {
        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: UIWebView.NavigationType.linkClicked.rawValue,
            .formSubmitted: UIWebView.NavigationType.formSubmitted.rawValue,
            .backForward: UIWebView.NavigationType.backForward.rawValue,
            .reload: UIWebView.NavigationType.reload.rawValue,
            .formResubmitted: UIWebView.NavigationType.formResubmitted.rawValue,
            .other: UIWebView.NavigationType.other.rawValue
        ]
    }

    // MARK: - Methods

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

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

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

        // 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() {
        webView?.goBack()
    }

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

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

    // 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"
        guard let cookie = webView.stringByEvaluatingJavaScript(from: cookieFetchJs) else {
            return completionHandler(nil, nil)
        }
        guard let cookieValue = AstroWebViewAdaptor.getCookieValueFromString(cookieName, cookie: cookie) else {
            return completionHandler(nil, nil)
        }
        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.stringByEvaluatingJavaScript(from: cookieDeleteJs)
        completionHandler()
    }

    private func guardWebView() {
        if webView == nil {
            preconditionFailure("Attempted to interact with web view without first calling registerWebView() with a non-nil web view")
        }
    }

    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.stringByEvaluatingJavaScript(from: receiveMessageJavascript)
    }

    override func load(_ request: URLRequest) {
        guardWebView()
        // TODO should not be used by the worker
        AstroLog.logger(AstroLog.WebAdaptor).info("Loading url: \(request.url != nil ? request.url!.absoluteString : "")")
        nextNavigationIsExplicit = true
        webView!.loadRequest(request)
    }

    @objc func processBridgeMessages() {
        guardWebView()
        processBridgeMessageJson(webView!.stringByEvaluatingJavaScript(from: "window.Astro.fetchMessages();") as AnyObject?, error: nil)
    }

    override func addScript(atURL url: String) {
        guardWebView()
        if webView!.isLoading || webView!.request == nil {
            scriptsToAdd.append(url)
        } else {
            injectScriptTag(in: url)
        }
    }

    private func injectScriptTag(in url: String) {
        guardWebView()
        webView!.stringByEvaluatingJavaScript(from: "var scriptTag = document.createElement('script'); scriptTag.src = '\(url)'; document.body.appendChild(scriptTag);")
    }

    private func injectPendingScripts() {
        for url in scriptsToAdd {
            injectScriptTag(in: url)
        }

        scriptsToAdd = []
    }

    private func chainedDelegateShouldStartLoadWithRequest(_ webView: UIWebView, request: URLRequest, navigationType: UIWebView.NavigationType) -> Bool {
        return chainedWebViewDelegate?.webView?(webView, shouldStartLoadWith: request, navigationType: navigationType) ?? true
    }

    // MARK: - UIWebViewDelegate

    func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebView.NavigationType) -> Bool {

        let currentNavigationIsExplicit = nextNavigationIsExplicit
        if let url = request.url {
            AstroLog.logger(AstroLog.WebAdaptor).debug("UIWebViewDelegate webView(_:shouldStartLoadWithRequest:navigationType:)" +
                "\n    request.URL: \(url)" +
                "\n    request.HTTPMethod: \(request.httpMethod!)" +
                "\n    request.mainDocumentURL: \(request.mainDocumentURL!)" +
                "\n    navigationType: \(navigationType)" +
                "\n    isLoading: \(isLoading)" +
                "\n    nextNavigationIsExplicit: \(nextNavigationIsExplicit)")

            // Reset before returning
            nextNavigationIsExplicit = false

            // Astro bridge
            if url.scheme == UIWebViewAdaptor.BridgeUrlScheme {
                processBridgeMessages()
                return false
            }

            // Chained UIWebView delegate handling
            if !chainedDelegateShouldStartLoadWithRequest(webView, request: request, navigationType: navigationType) {
                return false
            }

            return shouldAllowLoadForNavigationTypeHelper(navigationType.rawValue,
                currentNavigationIsExplicit: currentNavigationIsExplicit,
                url: url,
                request: request,
                decisionHandler: {(_) in })
        } else {
            return self.denyLoad("UIWebViewDelegate received request with nil url", url: nil, decisionHandler: { (_) in })
        }
    }

    func webView(_ webView: UIWebView, didFailLoadWithError error: Error) {
        failedNavigationWithError(error as NSError)
        chainedWebViewDelegate?.webView?(webView, didFailLoadWithError: error)
    }

    func webViewDidFinishLoad(_ webView: UIWebView) {
        AstroLog.logger(AstroLog.WebAdaptor).debug("UIWebViewDelegate webViewDidFinishLoad(_:)")
        chainedWebViewDelegate?.webViewDidFinishLoad?(webView)

        injectPendingScripts()

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

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

    func webViewDidStartLoad(_ webView: UIWebView) {
        AstroLog.logger(AstroLog.WebAdaptor).info("UIWebViewDelegate webViewDidStartLoad(_:)")
        chainedWebViewDelegate?.webViewDidStartLoad?(webView)
    }
}
