import WebKit

private enum WebViewError {
    case timeout
    case noInternet
    case unknown(underlyingError: NSError)

    var code: Int {
        switch self {
        case .timeout:
            return NSURLErrorTimedOut
        case .noInternet:
            return NSURLErrorNotConnectedToInternet
        case .unknown(let error):
            return error.code
        }
    }

    var description: String {
        switch self {
        case .timeout:
            return "Page load timed out"
        case .noInternet:
            return "No internet connection"
        case .unknown(let error):
            return error.localizedDescription
        }
    }
}

private extension WebViewError {
    func jsonObjectRepresentation() -> JSONObject {
        let error: JSONObject = [
            "description": self.description,
            "code": self.code
        ]

        return error
    }
}

public class WebViewPlugin: Plugin, ViewPlugin, WebBridgeDelegate, WebClientDelegate, AstroWebViewControllerDelegate {

    @objc let disposeWhenPopped: Bool
    @objc var hostsToShowManually = [String]()

    @objc var pageTimer: Timer?
    @objc var pageTimeoutDuration: TimeInterval = 10

    @objc var currentlyNavigatingBack = false

    @objc var typedViewController: AstroWebViewController?
    @objc public var viewController: UIViewController {
        return typedViewController!
    }

    @objc var webViewAdaptor: AstroWebViewAdaptor!
    @objc var keepAliveTimer: Timer?
    var parentPlugin: ParentPlugin?

    // Necessary so that all WebViewPlugins we create share the same cookies
    @objc static let webViewConfiguration: WKWebViewConfiguration = {
        // WKWebView with capturing (and even on some non-Mobified websites) has a bug where it 
        // ignores contentInset when the page initially loads but then "jumps" down to the 
        // correct place after a short period of time (1-5 sec). Injecting a <meta name="viewport"> 
        // tag _before_ the Mobify tag fixes this.
        // Note that we inject this script .AtDocumentStart. At that point the document tag has
        // been created, but is completely empty. We create a <head> tag which means that 
        // as the document HTML streams in, the web view will encounter a duplicate <head> tag.
        // WKWebView appears to ignore this duplicate <head> tag, but just FYI.
        let startSource = "var doc = document.documentElement;" +
            "var meta = document.createElement('meta');" +
            "meta.name = 'viewport';" +
            "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';" +
            "var head = document.head;" +
            "if (!head) { head = document.createElement('head'); doc.appendChild(head); } " +
            "head.appendChild(meta);"

        let userContentController = WKUserContentController()
        userContentController.addUserScript(WKUserScript(source: startSource, injectionTime: .atDocumentStart, forMainFrameOnly: true))

        let config = WKWebViewConfiguration()
        config.userContentController = userContentController
        config.processPool = WKProcessPool()

        return config
    }()

    var loaderPlugin: LoaderPlugin?
    @objc var loaderEnabled = true

    var webBridgeOwner: Addressable {
        return self
    }

    public required init(address: MessageAddress, messageBus: MessageBus, pluginResolver: PluginResolver, options: JSONObject?) {
        disposeWhenPopped = options?["disposeWhenPopped"]  as? Bool ?? false

        super.init(address: address, messageBus: messageBus, pluginResolver: pluginResolver, options: options)

        typedViewController = AstroWebViewController(webViewConfiguration: WebViewPlugin.webViewConfiguration)
        typedViewController?.delegate = self

        guard let webView = typedViewController?.webView else {
            fatalError("Unsupported web view: '\(typedViewController?.webView.debugDescription ?? "No webview")'")
        }

        webViewAdaptor = WKWebViewAdaptor(webView: webView)
        keepAliveTimer = setupKeepAliveTimer()

        webViewAdaptor.webBridgeDelegate = self
        webViewAdaptor.webClientDelegate = self

        addRpcMethodShim("allowExplicitNavigation") { [unowned self] params, respond in
            ////////// This will be autogenerated at some point //////////
            if let urlPatterns: [String] = MethodShimUtils.getArg(params, key: "urlPatterns", respond: respond) {
                self.allowExplicitNavigation(urlPatterns, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        addRpcMethodShim("navigate") { [unowned self] params, respond in
            ////////// This will be autogenerated at some point //////////
            if let url: String = MethodShimUtils.getArg(params, key: "url", respond: respond) {
                self.navigate(url, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

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

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

        addRpcMethodShim("setPageTimeoutDuration") { [unowned self] params, respond in
            ////////// This will be autogenerated at some point //////////
            if let duration: TimeInterval = MethodShimUtils.getArg(params, key: "timeoutDuration", respond: respond) {
                self.setPageTimeoutDuration(duration, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

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

        addRpcMethodShim("manuallyShowPageForHosts") { [unowned self] params, respond in
            ////////// This will be autogenerated at some point //////////
            if let hosts: [String] = MethodShimUtils.getArg(params, key: "hosts", respond: respond) {
                self.manuallyShowPageForHosts(hosts, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        addRpcMethodShim("setBackgroundColor") { [unowned self] params, respond in
            ////////// This will be autogenerated at some point //////////
            if let color: String = MethodShimUtils.getArg(params, key: "color", respond: respond) {
                self.setBackgroundColor(color, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        addRpcMethodShim("setLoaderPlugin") { [unowned self] params, respond in
            ////////// This will be autogenerated at some point //////////
            if let pluginAddress: String = MethodShimUtils.getArg(params, key: "address", respond: respond) {
                self.setLoaderPlugin(pluginAddress, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

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

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

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

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

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

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

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

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

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

        addAsyncRpcMethodShim("getCookie") { [unowned self] params, respond in
            ////////// This will be autogenerated at some point //////////
            if let cookieName: String = MethodShimUtils.getArg(params, key: "cookieName", respond: respond) {
                self.getCookie(cookieName, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }
    }

    private func redirectMessageFromBridge(message: RPCRequest, messageMethod: String, redirectAction: (JSONObject, RPCMethodCallback) -> Void) -> Bool {
        if message.to == WORKER_ADDRESS && message.method == messageMethod {
            let respond: RPCMethodCallback = { [weak self] result in
                let response = message.createResponse(result)
                self?.messageBus.send(response)
            }
            redirectAction(message.params, respond)
            return true
        }

        return false
    }

    private func redirectPwaNavigate(params: JSONObject, respond: RPCMethodCallback) {
        webViewAdaptor.saveOffset()
        guard let parentPlugin = parentPlugin else {
            respond(.result(NSNull()))
            return
        }

        parentPlugin.pwaNavigate(self, params: params, respond: respond)
    }

    private func redirectPwaRendered(params: JSONObject, respond: RPCMethodCallback) {
        guard let parentPlugin = parentPlugin else {
            respond(.result(NSNull()))
            return
        }

        parentPlugin.pwaRendered(respond: respond)
    }

    // MARK: - AstroWebViewControllerDelegate

    @objc func astroWebViewController(_ webViewController: AstroWebViewController, navigateWithRequest request: URLRequest, showLoader: Bool) {
        navigate(with: request, showLoader: showLoader)
    }

    @objc func astroWebViewControllerOnPluginRemoved(_ webViewController: AstroWebViewController) {
        if disposeWhenPopped {
            // Meta: This method should break all retain cycles between this
            //       plugin and any objects that have references to it. Once
            //       they are all broken deinit() will be called and ARC will
            //       reclaim this object and any owned objects.

            // Timer's retain a strong reference to their target, so we have to
            // invalidate it in order to allow this object to be dealloc'd
            pageTimer?.invalidate()
            keepAliveTimer?.invalidate()
            keepAliveTimer = nil
            pluginResolver.remove(instance: self)

            typedViewController = nil
        }
        hideLoader(animated: false)
    }

    @objc func astroWebViewController(_ webViewController: AstroWebViewController, updateWebView webView: UIView) {
        webViewAdaptor.registerWebView(webView)
    }

    @objc func astroWebViewControllerClearWebView() {
        webViewAdaptor.unregisterWebView()
    }

    @objc func astroWebViewControllerDidNavigateBack(_ controller: AstroWebViewController) {
        webViewAdaptor.didNavigateBack()
    }

    // MARK: -

    // The WKWebView has a "fun" bug where it goes blank at random times.
    // This keep-alive timer pings the webview periodically and reloads
    // it if it's gone blank.
    private func setupKeepAliveTimer() -> Timer {
        let interval = TimeInterval(1) // check on a 1 second interval
        let timer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(keepAliveTimerHandler(_:)), userInfo: nil, repeats: true)
        return timer
    }

    @objc func keepAliveTimerHandler(_ timer: Timer) {
        self.typedViewController?.reloadWebViewIfNeeded()
    }

    // Due to the way the mobify tag works the mobify-path cookie may be set
    // to an empty string, forcing the desktop site to load. In this case the
    // user will be caught in an endless watchdog loop since a mobified page
    // will never be rendered; mobifyPageRendered() will never be called.
    @objc func deleteMobifyPathCookieIfEmpty() {
        let cookieName = "mobify-path"
        webViewAdaptor.getCookie(named: cookieName) { cookieValue, _ in
            if cookieValue?.isEmpty == true {
                self.webViewAdaptor.deleteCookie(named: cookieName) {}
            }
        }
    }

    func receiveMessageFromBridge(_ bridgeMessage: BridgeMessage) {
        if let message = WebBridgeUtils.message(from: bridgeMessage, owner: self) {
            if (message.to != WORKER_ADDRESS) && (message.to != eventAddress) {
                // Bad web bridge, log this inappropriately addressed message
                AstroLog.logger(AstroLog.WebAdaptor).debug("Error: JavaScript in WebViewPlugin \(address) attempted to send message \(bridgeMessage) to \(address), it was not sent. JavaScript in WebViewPlugin instances may only send messages to the Worker address, or their own events address.")
                return
            }

            // Redirect specific messages from the bridge which need to handle internally (instead of sending them to the message bus)
            if let message = message as? RPCRequest {
                if redirectMessageFromBridge(message: message, messageMethod: "pwa-navigate", redirectAction: redirectPwaNavigate) {
                    return
                }
                if redirectMessageFromBridge(message: message, messageMethod: "pwa-rendered", redirectAction: redirectPwaRendered) {
                    return
                }
            }
            messageBus.send(message)
        }
    }

    override public func receive(_ response: RPCResponse) {
        WebBridgeUtils.send(response, toBridge: webViewAdaptor)
    }

    override func receive(_ event: EventMessage) {
        WebBridgeUtils.send(event, toBridge: webViewAdaptor)
    }

    @objc func pageDidStartLoading(_ url: URL) {
        pageTimer?.invalidate()
        pageTimer = Timer.scheduledTimer(timeInterval: pageTimeoutDuration, target: self, selector: #selector(triggerPageTimeout), userInfo: nil, repeats: false)

        showLoaderAnimated(true, targetUrl: url, completion: nil)
    }

    @objc func pageDidFinishLoading() {
        triggerNavigationCompleted()
        navigationRequestCompleted()
    }

    @objc func handleNavigationRequest(_ request: URLRequest) {
        deleteMobifyPathCookieIfEmpty()
        if request.httpMethod == "POST" {
            navigate(with: request)
            return
        }

        triggerNavigation(request)
    }

    @objc func notifyAppStoreLinkOpened() {
        // Often a page redirects a couple times before it actually gets to the app store
        // leaving the webview in a persistent loading state. Try to detect that and if
        // that's the case then pop that web view off the stack
        var loading = false
        if let wv = typedViewController?.webView {
            loading = wv.isLoading
        }
        if loading {
            parentPlugin?.removeChildPlugin(self)
        }
    }

    @objc func pageDidFailLoadWithError(_ error: NSError) {
        triggerNavigationFailedWithError(error)
        navigationRequestCompleted()
    }

    @objc func navigationRequestCompleted() {
        // Clear the timeout timer before checking shouldShowCurrentPageManually since we don't want a
        // timeout due to a slow adaptive/web page, or an intentional delay to calling showPage.
        pageTimer?.invalidate()

        // Note: This is only required for WKWebView
        typedViewController?.saveLastKnownUrl()

        if !shouldShowCurrentPageManually() {
            hideLoader()
        }

        // When stacking, we trigger back in the didPopToViewController,
        // but that delegate method doesn't fire when not using stacking,
        // so we explicitly call it here in the non-stacking case.
        if currentlyNavigatingBack {
            triggerBack()
            currentlyNavigatingBack = false
        }
    }

    @objc func latestSnapshot() -> UIView? {
        return typedViewController?.snapshot
    }

    // MARK: - triggered events
    private func triggerNavigation(_ request: URLRequest) {
        // Until we refactor android to not pass back isCurrentlyLoading
        // We will always return false in ios
        // We only trigger this if the WebClientDelegate has been asked
        // to handle the request ie. hasn't been loaded in the 
        // webview automatically
        let isCurrentlyLoading = false
        let url = currentUrl() ?? ""
        let params: JSONObject = [
            "currentUrl": url,
            "url": request.url?.absoluteString ?? "",
            "isCurrentlyLoading": isCurrentlyLoading
        ]
        trigger("navigate", params: params)
    }

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

    private func triggerNavigationCompleted() {
        let params: JSONObject = [
            "url": currentUrl() ?? ""
        ]
        trigger("navigationCompleted", params: params)
    }

    private func triggerNavigationFailedWithError(_ error: NSError) {
        guard let url = error.userInfo[NSURLErrorFailingURLStringErrorKey] as? String else {
            fatalError("The NSError provided to triggerNavigationFailedWithError does not contain a '\(NSURLErrorFailingURLStringErrorKey)' key!")
        }

        let error = WebViewError.unknown(underlyingError: error).jsonObjectRepresentation()
        let params: JSONObject = [
            "url": url,
            "error": error
        ]
        trigger("navigationFailed", params: params)
    }

    @objc func triggerPageTimeout() {
        webViewAdaptor.stopLoading()
        deleteMobifyPathCookieIfEmpty()
        let url = self.currentUrl() ?? ""
        self.triggerNavigationFailedForURL(url, withError: .timeout)
    }

    @objc func notifyNoInternetConnection(_ url: String) {
        self.triggerNavigationFailedForURL(url, withError: .noInternet)
    }

    fileprivate func triggerNavigationFailedForURL(_ url: String, withError error: WebViewError) {
        let params: JSONObject = [
            "url": url,
            "error": error.jsonObjectRepresentation()
        ]
        trigger("navigationFailed", params: params)
    }

    // MARK: -

    @objc func canGoBack() -> Bool {
        return webViewAdaptor.canGoBack
    }

    @objc func currentUrl() -> String? {
        if let url = webViewAdaptor.currentURL {
            return url.absoluteString
        }
        return nil
    }

    // MARK: - RPC Methods

    // @RpcMethod
    func allowExplicitNavigation(_ urlPatterns: [String], respond: RPCMethodCallback) {
        webViewAdaptor.allowExplicitNavigation(urlPatterns)
    }

    // @RpcMethod
    func navigate(_ url: String, respond: RPCMethodCallback) {
        navigateInternal(url, respond: respond)
    }

    // @RpcMethod
    func back(_ respond: RPCMethodCallback) {
        webViewAdaptor.goBack()
        currentlyNavigatingBack = true
    }

    // @RpcMethod
    func reload(_ respond: RPCMethodCallback) {
        webViewAdaptor.reload()
    }

    // @RpcMethod
    func setPageTimeoutDuration(_ duration: TimeInterval, respond: RPCMethodCallback) {
        pageTimeoutDuration = duration
    }

    // @RpcMethod
    func showPage(_ respond: RPCMethodCallback) {
        hideLoader()
    }

    // @RpcMethod
    func manuallyShowPageForHosts(_ hosts: [String], respond: RPCMethodCallback) {
        hostsToShowManually = hosts
    }

    // @RpcMethod
    func setBackgroundColor(_ color: String, respond: RPCMethodCallback) {
        if let color = UIColor(hex: color) {
            typedViewController?.view.backgroundColor = color
        }
    }

    // @RpcMethod
    func setLoaderPlugin(_ pluginAddress: MessageAddress, respond: RPCMethodCallback) {
        if let plugin: LoaderPlugin = pluginResolver.pluginInstanceByAddress(pluginAddress, respond: respond) {
            loaderPlugin = plugin
        }
    }

    // @RpcMethod
    func showScrollBars(_ respond: RPCMethodCallback) {
        webViewAdaptor.setScrollBarsEnabled(true)
    }

    // @RpcMethod
    func hideScrollBars(_ respond: RPCMethodCallback) {
        webViewAdaptor.setScrollBarsEnabled(false)
    }

    // @RpcMethod
    func enableScrollBounce(_ respond: RPCMethodCallback) {
        webViewAdaptor.setScrollBounces(true)
    }

    // @RpcMethod
    func disableScrollBounce(_ respond: RPCMethodCallback) {
        webViewAdaptor.setScrollBounces(false)
    }

    // @RpcMethod
    func enableScrolling(_ respond: RPCMethodCallback) {
        webViewAdaptor.setScrolling(true)
    }

    // @RpcMethod
    func disableScrolling(_ respond: RPCMethodCallback) {
        webViewAdaptor.setScrolling(false)
    }

    // @RpcMethod
    func enableLoader(_ respond: RPCMethodCallback) {
        loaderEnabled = true
    }

    // @RpcMethod
    func disableLoader(_ respond: RPCMethodCallback) {
        loaderEnabled = false
    }

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

    // @AsyncRpcMethod
    func getCookie(_ cookieName: String, respond: @escaping RPCMethodCallback) {
        webViewAdaptor.getCookie(named: cookieName) { cookieValue, error in
            if let cookieValue = cookieValue {
                respond(.result(cookieValue))
            } else if let error = error {
                return respond(.error(error))
            } else {
                respond(.result(NSNull()))
            }
        }
    }

    // MARK: - Internal methods

    @objc func appendURLComponent(_ originalURL: URL, param: String?, separator: String) -> URL {
        guard let slug = param else {
            return originalURL
        }
        let urlString = originalURL.absoluteString
        // NSURL only allows one fragment in a URL so if there is already one present we just return the original url
        if urlString.contains("#") {
            AstroLog.logger(AstroLog.Plugins).error("Attempted to append '#\(slug)' to '\(urlString)'")
            return originalURL
        }
        return URL(string: urlString + separator + slug)!
    }

    private func navigateInternal(_ urlString: String, respond: RPCMethodCallback) {
        guard let url = URL(string: urlString) else {
            respond(.error("Invalid URL: \(urlString)"))
            return
        }

        var loadUrl = url
        if url.isFileURL {
            // Resolve the file url to a bundle-relative filesystem URL
            if let resourceUrl = AstroViewController.mainBundle.url(forResource: url.path, withExtension: nil) {
                loadUrl = resourceUrl
            }

            // Restore the url fragment if one existed
            loadUrl = appendURLComponent(loadUrl, param: url.query, separator: "?")
            loadUrl = appendURLComponent(loadUrl, param: url.fragment, separator: "#")
        } else if WKWebViewAdaptor.isITunesURL(url) || WKWebViewAdaptor.isDeepLinkSchemeURL(url) {
            // This is an app store link or deep link
            AstroLog.logger(AstroLog.WebAdaptor).info("Intercepted itunes/deep link url and prevented navigation \(url)")
            UIApplication.shared.openURL(url)
            respond(.result(NSNull()))
            return
        }

        navigate(with: URLRequest(url: loadUrl))
        respond(.result(NSNull()))
    }

    @objc func navigate(with request: URLRequest, showLoader: Bool = true) {
        if showLoader {
            showLoaderAnimated(true, targetUrl: request.url!) { _ in
                self.webViewAdaptor.load(request)
            }
        } else {
            self.webViewAdaptor.load(request)
        }
    }

    private func shouldShowCurrentPageManually() -> Bool {
        if let url = webViewAdaptor.currentURL, let host = url.host {
            return hostsToShowManually.contains(host)
        }
        return false
    }

    private func showLoaderAnimated(_ animated: Bool, targetUrl: URL, completion: ((Bool) -> Void)?) {
        guard loaderEnabled else {
            completion?(true)
            return
        }

        guard !isFragmentChange(for: webViewAdaptor.lastLoadedUrl, navigatingToTargetUrl: targetUrl) else {
            completion?(true)
            return
        }

        guard let loaderPlugin = loaderPlugin else {
            completion?(true)
            return
        }

        loaderPlugin.setURL(targetUrl)

        if let typedViewController = typedViewController, !typedViewController.children.contains(loaderPlugin.viewController) {
            typedViewController.addChild(loaderPlugin.viewController)
            loaderPlugin.viewController.didMove(toParent: typedViewController)
            typedViewController.view.addSubview(loaderPlugin.viewController.view)
            loaderPlugin.viewController.view.pinToSuperviewEdges()
        }

        loaderPlugin.show(animated: animated, completion: completion)
    }

    private func hideLoader(animated: Bool = true) {
        if let loaderPlugin = loaderPlugin {
            loaderPlugin.hide(animated: true) { _ in
                loaderPlugin.viewController.willMove(toParent: nil)
                loaderPlugin.viewController.removeFromParent()
                loaderPlugin.viewController.view.removeFromSuperview()
            }
        }
    }

    // Note: these alert/confirm/input handlers are WKWebView specific (UIWebView does not require these)
    @objc func showAlertPanelWithMessage(_ message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
        let alertActionOK = UIAlertAction(title: "OK", style: .default) { (_) -> Void in
            completionHandler()
        }
        alertController.addAction(alertActionOK)

        typedViewController?.present(alertController, animated: true, completion: nil)
    }

    @objc func showConfirmPanelWithMessage(_ message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
        let confirmController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet)

        let alertActionOK = UIAlertAction(title: "OK", style: .default) { (_) -> Void in
            completionHandler(true)
        }
        let alertActionCancel = UIAlertAction(title: "Cancel", style: .cancel) { (_) -> Void in
            completionHandler(false)
        }
        confirmController.addAction(alertActionOK)
        confirmController.addAction(alertActionCancel)

        typedViewController?.present(confirmController, animated: true, completion: nil)
    }

    @objc func showTextInputPanelWithPrompt(_ message: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
        let textInputController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
        var inputTextField: UITextField!

        textInputController.addTextField { (textField: UITextField!) -> Void in
            inputTextField = textField
            if let defaultText = defaultText {
                textField.text = defaultText
            }
        }

        let inputActionOK = UIAlertAction(title: "OK", style: .default) { (_) in
            completionHandler(inputTextField.text)
        }
        let inputActionCancel = UIAlertAction(title: "Cancel", style: .cancel) { (_) in
            completionHandler("")
        }
        textInputController.addAction(inputActionOK)
        textInputController.addAction(inputActionCancel)

        typedViewController?.present(textInputController, animated: true, completion: nil)
    }
}
