//
//  HeaderBarPlugin.swift
//  Astro
//
//  Created by Mark Sandstrom on 5/5/15.
//  Copyright (c) 2015 Mobify Research & Development Inc. All rights reserved.
//

import Foundation

private var HeaderBarPluginContext = 0

enum HeaderLocation {
    case left
    case center
    case right
}

protocol HeaderContentCoordinator: class {
    func suspendUpdates()
    func resumeUpdates()
    func discardPendingUpdatesAndResume()

    func pushHeaderContent(_ headerContent: HeaderContent, coordinator: UIViewControllerTransitionCoordinator?)
    func popHeaderContentWithCoordinator(_ coordinator: UIViewControllerTransitionCoordinator?, isSnapshot: Bool)
    func popToRootHeaderContentWithCoordinator(_ coordinator: UIViewControllerTransitionCoordinator?)
    func removeHeaderContentAtIndex(_ index: Int)
    func getLastHeaderContent() -> HeaderContent?
}

protocol HeaderItem {
    var headerContentItem: HeaderContentItem? { get set }
}

protocol HeaderBarViewControllerDelegate: class {
   func triggerBackEvent()
}

private class TitleView: UIView {
    @objc var subview: UIView? {
        get {
            return (self.subviews).last
        }
        set {
            if let currentSubview = (self.subviews).last {
                currentSubview.removeFromSuperview()
            }

            if let newSubview = newValue {
                addSubview(newSubview)
                let oldCenter = center
                frame.size = newSubview.frame.size
                center = oldCenter
            }
        }
    }
}

public class HeaderBarPlugin: Plugin, ViewPlugin {
    private let horizontalPadding: CGFloat = 12.0

    private let typedViewController = HeaderBarViewController()

    @objc public var viewController: UIViewController {
        return typedViewController
    }

    // I would like to move this reference into the HeaderContentCoordinator protocol
    // It is challenging to do this in a simple way b/c it is a weak reference
    weak var navigationPlugin: NavigationPlugin?

    // true if coordinating with the HeaderContentCoordinator protocol
    fileprivate var isCoordinating = false

    private var backButtonTextIsHidden = false
    private var showingBottomBorder = true

    fileprivate var updatesCancelled = false
    @objc let updateQueue: OperationQueue = {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1 // make it a serial queue
        queue.isSuspended = false
        return queue
    }()

    @objc var navigationBar: UINavigationBar {
        return typedViewController.navigationBar
    }

    private var shadowImage: UIImage?
    private var backgroundImage: UIImage?

    var headerContentStack = [HeaderContent]()

    @objc var navigationItems: [UINavigationItem] {
        return headerContentStack.map(navigationItemForHeaderContent)
    }

    @objc var hasHeaderContent: Bool {
        return headerContentStack.count > 0
    }

    var currentHeaderContent: HeaderContent? {
        return headerContentStack.count > 0 ? headerContentStack[headerContentStack.count-1] : nil
    }

    func pushHeaderContent(_ headerContent: HeaderContent, animated: Bool) {
        headerContentStack.append(headerContent)
        navigationBar.pushItem(navigationItemForHeaderContent(currentHeaderContent!), animated: animated)
    }

    @discardableResult
    func popHeaderContent(animated: Bool, isSnapshot: Bool=false) -> HeaderContent {
        // A common code path for back navigation both via Back button and Swipe
        typedViewController.navigationBackCoordinated = true

        let headerContent = headerContentStack.removeLast()
        navigationBar.popItem(animated: animated)
        if hasHeaderContent && isSnapshot {
            let newNavigationItem = navigationItemForHeaderContent(currentHeaderContent!)
            if let rightIcon = currentHeaderContent?.rightIcon {
                if case .plugin = rightIcon.content {
                     navigationBar.items?.last?.setRightBarButtonItems(newNavigationItem.rightBarButtonItems, animated: animated)
                }
            }
            if let leftIcon = currentHeaderContent?.leftIcon {
                if case .plugin = leftIcon.content {
                     navigationBar.items?.last?.setLeftBarButtonItems(newNavigationItem.leftBarButtonItems, animated: animated)
                }
            }
        }
        return headerContent
    }

    @objc func popToRootHeaderContent(animated: Bool) {
        headerContentStack = [headerContentStack.first!]
        navigationBar.setItems([navigationBar.items!.first!], animated: animated)
    }

    func updateCurrentHeaderContent(_ headerContent: HeaderContent, animated: Bool) {
        assert(hasHeaderContent, "No header content to update! You must navigate() before setting any header items.")

        let previousHeaderContent = headerContentStack.removeLast()
        headerContentStack.append(headerContent)

        let currentHeaderContent = headerContent

        let newNavigationItem = navigationItemForHeaderContent(currentHeaderContent)
        let currentNavigationItem = navigationBar.items!.last!

        // Update the existing navigation item bar button items and title view subview in place. Swapping
        // out the entire navigation item doesn't allow us to control the animations.

        // Animate left item only if it has been changed. Or else it would blink with fade animation
        let leftNavigationItemAnimated = animated && (previousHeaderContent.leftIcon != currentHeaderContent.leftIcon)
        currentNavigationItem.setLeftBarButtonItems(newNavigationItem.leftBarButtonItems, animated: leftNavigationItemAnimated)

        // Native title doesn't support animation
        if let title = newNavigationItem.title {
            currentNavigationItem.titleView = nil
            currentNavigationItem.title = title
        } else if let newTitleView = newNavigationItem.titleView as? TitleView {
            if currentNavigationItem.titleView == nil {
                currentNavigationItem.titleView = newTitleView
                newNavigationItem.titleView = nil // We have to remove the titleView from its previous owner, otherwise our titleView subviews seem to be removed
            } else if let titleView = currentNavigationItem.titleView as? TitleView {
                titleView.subview = newTitleView.subview
            }
        }

        currentNavigationItem.backBarButtonItem = newNavigationItem.backBarButtonItem

        // Animate right item only if it has been changed. Or else it would blink with fade animation
        let rightNavigationItemAnimated = animated && (previousHeaderContent.rightIcon != currentHeaderContent.rightIcon)
        currentNavigationItem.setRightBarButtonItems(newNavigationItem.rightBarButtonItems, animated: rightNavigationItemAnimated)
    }

    func headerButtonWithHeaderContentItem(_ item: HeaderContentItem, location: HeaderLocation) -> UIControl {
        var button: UIControl
        var addTargetToButton = true

        switch item.content {
        case .image(let image):
            let uiButton = HeaderButton(headerContentItem: item)
            let buttonIcon = (Localization.isRightToLeft && item.shouldFlipOnRtl)
                ? flipImage(image)
                : image
            uiButton.setImage(buttonIcon, for: .normal)
            uiButton.setImage(buttonIcon, for: .highlighted)
            button = uiButton
            adjustTapTargetForButton(button, contentType: .image(image))
        case .title(let title):
            let uiButton = HeaderButton(headerContentItem: item)
            uiButton.setTitle(Localization.translate(title), for: .normal)
            uiButton.setTitleColor(navigationBar.tintColor, for: .normal)
            button = uiButton
            adjustTapTargetForButton(button, contentType: .title(title))
        case .plugin(let viewPlugin):
            // UINavigationItem's do not know how to properly use constraints
            // to position themselves. Due to this inability we have to
            // place the plugin views inside the navigation bar ourselves
            let subViewSize = viewPlugin.viewController.view.frame.size
            let xValueForPluginButton = calculateXValueForHeaderButton(location, subViewWidth: subViewSize.width)
            let viewRect = CGRect(x: xValueForPluginButton, y: 0.0, width: subViewSize.width, height: subViewSize.height)

            let pluginButton = HeaderControlView(frame: viewRect, headerContentItem: item, autoresizingMask: autoresizingMaskForPluginButton(location))

            addTargetToButton = (viewPlugin as? HandlesUserInteraction) == nil

            // block the plugin view's touch events so that the button's events can pass through
            viewPlugin.viewController.view.isUserInteractionEnabled = !addTargetToButton

            pluginButton.addSubview(viewPlugin.viewController.view)
            button = pluginButton
        }

        if addTargetToButton {
            button.addTarget(self, action: #selector(tapHeaderButton(_:)), for: .touchUpInside)
        }

        button.isHidden = item.hidden

        return button
    }

    private func adjustTapTargetForButton(_ button: UIControl, contentType: HeaderContentItemContent) {
        //  We handle padding and tap targets differently for buttons
        //  set with an image, text, or plugin:
        //
        //  (1) Image: Expand the tap target to be a min of 44×44. Icon
        //      assets are expected to be 22×22 for proper behavior.
        //
        //      +--(>= 44)--+
        //      |  +-----+  |   Note: The button width will match the width
        //      |  |image|  |         of the content if it exceeds 44px.
        //      |  |/////| (44)
        //      |  +-----+  |
        //      +-----------+
        //
        //  (2) Text: Wrap button text, expand button height to 44px and add
        //      a padding of 12px on the left and right side of the button.
        //
        //      +--+--------+--+
        //      |  |        |  |
        //      |  | Button |  |
        //      |  |  Text  | (44)
        //      |  |        |  |
        //      +--+--------+--+
        //       ^            ^
        //      (12)         (12)
        //
        //  (3) Plugin: Take plugin as is, and allow plugin to handle padding.
        let minButtonSize = CGSize(width: 44.0, height: 44.0)
        button.sizeToFit()
        button.frame.size.height = minButtonSize.height

        switch contentType {
        case .image:
            if button.frame.size.width <= minButtonSize.width {
                button.frame.size.width = minButtonSize.width
            }
        case .title:
            button.frame.size.width += horizontalPadding * 2
        case .plugin:
            // Do nothing
            break
        }
    }

    private func autoresizingMaskForPluginButton(_ location: HeaderLocation) -> UIView.AutoresizingMask {
        let commonAutoResizingMask: UIView.AutoresizingMask = [UIView.AutoresizingMask.flexibleTopMargin, UIView.AutoresizingMask.flexibleBottomMargin]
        var autoResizingMask: UIView.AutoresizingMask
        switch location {
        case .center:
            autoResizingMask = commonAutoResizingMask
        case .right:
            autoResizingMask = commonAutoResizingMask.union(UIView.AutoresizingMask.flexibleRightMargin)
        case .left:
            autoResizingMask = commonAutoResizingMask.union(UIView.AutoresizingMask.flexibleLeftMargin)
        }
        return autoResizingMask
    }

    private func calculateXValueForHeaderButton(_ location: HeaderLocation, subViewWidth: CGFloat) -> CGFloat {
        switch location {
        case .center:
            // This value is zero rather that half way along the navigation bar
            // because the center content is being set in the
            // UINavigationItem.titleView seems to know how to
            // center this view in the navigation bar
            return 0.0
        case .right:
            //Passing in a UIView into UINavigationItem.rightBarButtonItems doesn't
            //know how to position a general view properly in the navigation bar
            //So we need to position the Plugin's views ourselves inside the navigation bar
            return navigationBar.frame.size.width - subViewWidth
        case .left:
            //Passing in a UIView into UINavigationItem.leftBarButtonItems doesn't
            //know how to position a general view properly in the navigation bar
            //So we need to position the Plugin's views ourselves inside the navigation bar
            return 0
        }
    }

    @objc func tapHeaderButton(_ sender: UIControl) {
        if let headerItem = sender as? HeaderItem {
            if let item = headerItem.headerContentItem {
                if let navigationPlugin = navigationPlugin {
                    navigationPlugin.snapshotTopPlugin()
                }
                trigger("click:\(item.id)")
            }
        }
    }

    func navigationItemForHeaderContent(_ headerContent: HeaderContent) -> UINavigationItem {
        let navigationItem = UINavigationItem()
        let negativePadding = UIBarButtonItem(barButtonSystemItem:UIBarButtonItem.SystemItem.fixedSpace, target: nil, action: nil)
        negativePadding.width = -horizontalPadding

        navigationItem.backBarButtonItem = createBackButton()

        let titleView = TitleView()

        if let centerIcon = headerContent.centerIcon {
            switch centerIcon.content {
            case .image:
                titleView.subview = headerButtonWithHeaderContentItem(centerIcon, location: .center)
                navigationItem.titleView = titleView
            case .title(let title):
                navigationItem.titleView = nil
                navigationItem.title = Localization.translate(title)
            case .plugin:
                titleView.subview = headerButtonWithHeaderContentItem(centerIcon, location: .center)
                navigationItem.titleView = titleView
            }
        }

        if let leftIcon = headerContent.leftIcon {
            navigationItem.leftBarButtonItems = [
                negativePadding,
                UIBarButtonItem(customView: headerButtonWithHeaderContentItem(leftIcon, location: .left))
            ]
        }

        if let rightIcon = headerContent.rightIcon {
            navigationItem.rightBarButtonItems = [
                negativePadding,
                UIBarButtonItem(customView: headerButtonWithHeaderContentItem(rightIcon, location: .right))
            ]
        }

        return navigationItem
    }

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

        self.addRpcMethodShim("setLeftIcon") { params, respond in
            ////////// This will be autogenerated at some point //////////
            guard let url: String = MethodShimUtils.getArg(params, key: "url", respond: respond),
                let id: String = MethodShimUtils.getArg(params, key: "id", respond: respond),
                let shouldFlipOnRtl: Bool = MethodShimUtils.getArg(params, key: "shouldFlipOnRtl", respond: respond) else {
                    return
            }
            self.setLeftIcon(url, id: id, shouldFlipOnRtl: shouldFlipOnRtl, respond: respond)
            /////////////////////////////////////////////////////////////
        }

        self.addRpcMethodShim("setLeftTitle") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let title: String = MethodShimUtils.getArg(params, key: "title", respond: respond) {
                if let id: String = MethodShimUtils.getArg(params, key: "id", respond: respond) {
                    self.setLeftTitle(title, id: id, respond: respond)
                }
            }
            /////////////////////////////////////////////////////////////
        }

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

        self.addRpcMethodShim("setRightIcon") { params, respond in
            ////////// This will be autogenerated at some point //////////
            guard let url: String = MethodShimUtils.getArg(params, key: "url", respond: respond),
                let id: String = MethodShimUtils.getArg(params, key: "id", respond: respond),
                let shouldFlipOnRtl: Bool = MethodShimUtils.getArg(params, key: "shouldFlipOnRtl", respond: respond) else {
                return
            }
            self.setRightIcon(url, id: id, shouldFlipOnRtl: shouldFlipOnRtl, respond: respond)
            /////////////////////////////////////////////////////////////
        }

        self.addRpcMethodShim("setRightTitle") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let title: String = MethodShimUtils.getArg(params, key: "title", respond: respond) {
                if let id: String = MethodShimUtils.getArg(params, key: "id", respond: respond) {
                    self.setRightTitle(title, id: id, respond: respond)
                }
            }
            /////////////////////////////////////////////////////////////
        }

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

        self.addRpcMethodShim("setCenterIcon") { params, respond in
            ////////// This will be autogenerated at some point //////////
            guard let url: String = MethodShimUtils.getArg(params, key: "url", respond: respond),
                let id: String = MethodShimUtils.getArg(params, key: "id", respond: respond),
                let shouldFlipOnRtl: Bool = MethodShimUtils.getArg(params, key: "shouldFlipOnRtl", respond: respond) else {
                return
            }
            self.setCenterIcon(url, id: id, shouldFlipOnRtl: shouldFlipOnRtl, respond: respond)
            /////////////////////////////////////////////////////////////
        }

        self.addRpcMethodShim("setCenterTitle") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let title: String = MethodShimUtils.getArg(params, key: "title", respond: respond) {
                if let id: String = MethodShimUtils.getArg(params, key: "id", respond: respond) {
                    self.setCenterTitle(title, id: id, respond: respond)
                }
            }
            /////////////////////////////////////////////////////////////
        }

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

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

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

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

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

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

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

        self.addRpcMethodShim("setBackgroundColor") { 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)
            }
            /////////////////////////////////////////////////////////////
        }

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

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

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

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

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

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

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

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

        Localization.addLocaleChangedListener(self)

        // Add a default header content so that the header bar can used without coordinating
        // with the HeaderContentCoordinator protocol
        pushHeaderContent(HeaderContent(), animated: false)

        typedViewController.delegate = self
        updateQueue.addObserver(self, forKeyPath: "operationCount", options: [.new, .old], context: &HeaderBarPluginContext)
    }

    deinit {
        updateQueue.removeObserver(self, forKeyPath: "operationCount")
    }

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

        if keyPath == "operationCount" {
            if let newValue = change?[NSKeyValueChangeKey.newKey] as? Int,
                let oldValue = change?[NSKeyValueChangeKey.oldKey] as? Int {
                    if oldValue > 0 && newValue == 0 && updatesCancelled {
                        // Reset the flag when we get to 0 operations and we were cancelling pending updates
                        updatesCancelled = false
                    }
            }
        }
    }

    // MARK: - Helper functions

    private static func itemOptionsForKey(_ itemOptionKey: String, itemOption: String, id: String, shouldFlipOnRtl: Bool = false) -> JSONObject {
        return [itemOptionKey: itemOption, HeaderContentItem.IDKey: id, HeaderContentItem.shouldFlipOnRtlKey: shouldFlipOnRtl]
    }

    private static func updateLeftContentItem(_ headerContent: inout HeaderContent, headerContentItem: HeaderContentItem) {
        headerContent.leftIcon = headerContentItem
    }

    private static func updateCenterContentItem(_ headerContent: inout HeaderContent, headerContentItem: HeaderContentItem) {
        headerContent.centerIcon = headerContentItem
    }

    private static func updateRightContentItem(_ headerContent: inout HeaderContent, headerContentItem: HeaderContentItem) {
        headerContent.rightIcon = headerContentItem
    }

    private static func updateRightContentItemHidden(_ headerContent: inout HeaderContent, hidden: Bool) {
        // Not using if let/var b/c rightIcon is a struct. If if let/var was used then a value (not a reference)
        // to the object would be returned the change to the hidden field wouldn't be persisted
        if headerContent.rightIcon != nil {
            headerContent.rightIcon!.hidden = hidden
        }
    }

    private static func updateLeftContentItemHidden(_ headerContent: inout HeaderContent, hidden: Bool) {
        // Not using if let/var b/c rightIcon is a struct. If if let/var was used then a value (not a reference)
        // to the object would be returned the change to the hidden field wouldn't be persisted
        if headerContent.leftIcon != nil {
            headerContent.leftIcon!.hidden = hidden
        }
    }

    private func updateIconVisibility(_ hidden: Bool, updateContentItemHidden: ((_ headerContent: inout HeaderContent, _ hidden: Bool) -> Void)) {
        if var currentHeaderContent = self.currentHeaderContent {
            updateContentItemHidden(&currentHeaderContent, hidden)
            self.updateCurrentHeaderContent(currentHeaderContent, animated: true)
        }
    }

    private func updateCurrentHeaderContentItem(_ itemOptions: JSONObject, respond: RPCMethodCallback, updateContentItem: @escaping ((_ headerContent: inout HeaderContent, _ headerContentItem: HeaderContentItem) -> Void)) {
        if let item = HeaderContentItem(jsonObject: itemOptions, pluginResolver: pluginResolver, respond: respond) {
            // We use an update queue here so that we can suspend processing updates if we need to
            updateQueue.addOperation({
                // No need to do any work if we're cancelling updates.
                if self.updatesCancelled {
                    AstroLog.logger(AstroLog.Plugins).debug("[HeaderBarPlugin] Updates cancelled. Skipping operation for item: \(item)")
                    return
                }

                // Operation queue tasks are _always_ run on a NSOperationQueue-managed thread (ie. non-UI)
                DispatchQueue.main.sync {
                    if var headerContent = self.currentHeaderContent {
                        updateContentItem(&headerContent, item)
                        self.updateCurrentHeaderContent(headerContent, animated: true)
                    }
                }
            })
        }
    }

    // @RpcMethod
    func setLeftIcon(_ url: String, id: String, shouldFlipOnRtl: Bool, respond: RPCMethodCallback) {
        let itemOptions = HeaderBarPlugin.itemOptionsForKey(HeaderContentItem.imageKey, itemOption: url, id: id, shouldFlipOnRtl: shouldFlipOnRtl)

        updateCurrentHeaderContentItem(itemOptions, respond: respond, updateContentItem: HeaderBarPlugin.updateLeftContentItem)
    }

    // @RpcMethod
    func setLeftTitle(_ title: String, id: String, respond: RPCMethodCallback) {
        let itemOptions = HeaderBarPlugin.itemOptionsForKey(HeaderContentItem.titleKey, itemOption: title, id: id)

        updateCurrentHeaderContentItem(itemOptions, respond: respond, updateContentItem: HeaderBarPlugin.updateLeftContentItem)
    }

    // @RpcMethod
    func setLeftPlugin(_ pluginAddress: String, id: String, respond: RPCMethodCallback) {
        let itemOptions = HeaderBarPlugin.itemOptionsForKey(HeaderContentItem.pluginAddressKey, itemOption: pluginAddress, id: id)

        updateCurrentHeaderContentItem(itemOptions, respond: respond, updateContentItem: HeaderBarPlugin.updateLeftContentItem)
    }

    // @RpcMethod
    func setCenterIcon(_ url: String, id: String, shouldFlipOnRtl: Bool, respond: RPCMethodCallback) {
        let itemOptions = HeaderBarPlugin.itemOptionsForKey(HeaderContentItem.imageKey, itemOption: url, id: id, shouldFlipOnRtl: shouldFlipOnRtl)

        updateCurrentHeaderContentItem(itemOptions, respond: respond, updateContentItem: HeaderBarPlugin.updateCenterContentItem)
    }

    // @RpcMethod
    func setCenterTitle(_ title: String, id: String, respond: RPCMethodCallback) {
        let itemOptions = HeaderBarPlugin.itemOptionsForKey(HeaderContentItem.titleKey, itemOption: title, id: id)

        updateCurrentHeaderContentItem(itemOptions, respond: respond, updateContentItem: HeaderBarPlugin.updateCenterContentItem)
    }

    // @RpcMethod
    func setCenterPlugin(_ pluginAddress: String, id: String, respond: RPCMethodCallback) {
        let itemOptions = HeaderBarPlugin.itemOptionsForKey(HeaderContentItem.pluginAddressKey, itemOption: pluginAddress, id: id)

        updateCurrentHeaderContentItem(itemOptions, respond: respond, updateContentItem: HeaderBarPlugin.updateCenterContentItem)
    }

    // @RpcMethod
    func setRightIcon(_ url: String, id: String, shouldFlipOnRtl: Bool, respond: RPCMethodCallback) {
        let itemOptions = HeaderBarPlugin.itemOptionsForKey(HeaderContentItem.imageKey, itemOption: url, id: id, shouldFlipOnRtl: shouldFlipOnRtl)

        updateCurrentHeaderContentItem(itemOptions, respond: respond, updateContentItem: HeaderBarPlugin.updateRightContentItem)
    }

    // @RpcMethod
    func setRightTitle(_ title: String, id: String, respond: RPCMethodCallback) {
        let itemOptions = HeaderBarPlugin.itemOptionsForKey(HeaderContentItem.titleKey, itemOption: title, id: id)

        updateCurrentHeaderContentItem(itemOptions, respond: respond, updateContentItem: HeaderBarPlugin.updateRightContentItem)
    }

    // @RpcMethod
    func setRightPlugin(_ pluginAddress: String, id: String, respond: RPCMethodCallback) {
        let itemOptions = HeaderBarPlugin.itemOptionsForKey(HeaderContentItem.pluginAddressKey, itemOption: pluginAddress, id: id)

        updateCurrentHeaderContentItem(itemOptions, respond: respond, updateContentItem: HeaderBarPlugin.updateRightContentItem)
    }

    // @RpcMethod
    func showLeftIcon(_ respond: RPCMethodCallback) {
        updateIconVisibility(false, updateContentItemHidden: HeaderBarPlugin.updateLeftContentItemHidden)
    }

    // @RpcMethod
    func hideLeftIcon(_ respond: RPCMethodCallback) {
        updateIconVisibility(true, updateContentItemHidden: HeaderBarPlugin.updateLeftContentItemHidden)
    }

    // @RpcMethod
    func showRightIcon(_ respond: RPCMethodCallback) {
        updateIconVisibility(false, updateContentItemHidden: HeaderBarPlugin.updateRightContentItemHidden)
    }

    // @RpcMethod
    func hideRightIcon(_ respond: RPCMethodCallback) {
        updateIconVisibility(true, updateContentItemHidden: HeaderBarPlugin.updateRightContentItemHidden)
    }

    // @RpcMethod
    func showCenterIcon(_ respond: RPCMethodCallback) {
        AstroLog.logger(AstroLog.Plugins).debug("showCenterIcon() is not yet implemented on iOS")
    }

    // @RpcMethod
    func hideCenterIcon(_ respond: RPCMethodCallback) {
        AstroLog.logger(AstroLog.Plugins).debug("hideCenterIcon() is not yet implemented on iOS")
    }

    // @RpcMethod
    func setBackgroundColor(_ color: String, respond: RPCMethodCallback) {
        if let backgroundColor = UIColor(hex: color) {
            navigationBar.barTintColor = backgroundColor
        } else {
            respond(.error("Invalid hex color provided: '\(color)'."))
        }
    }

    // @RpcMethod
    func setOpaque(_ respond: RPCMethodCallback) {
        navigationBar.isTranslucent = false
    }

    // @RpcMethod
    func setTranslucent(_ respond: RPCMethodCallback) {
        navigationBar.isTranslucent = true
    }

    // @RpcMethod
    func showBottomBorder(_ respond: RPCMethodCallback) {
        navigationBar.shadowImage = nil
        navigationBar.setBackgroundImage(nil, for: UIBarMetrics.default)
        showingBottomBorder = true
    }

    // @RpcMethod
    func hideBottomBorder(_ respond: RPCMethodCallback) {
        // To hide the bottom border we need to set this to false.
        navigationBar.isTranslucent = false

        // We could save the original values for these two settings, but it turns out that before
        // this overwrite they are always nil. Since we don't have a way to set them currently,
        // no point in saving them. K.I.S.S.
        navigationBar.shadowImage = UIImage()
        navigationBar.setBackgroundImage(UIImage(), for: .default)
        showingBottomBorder = false
    }

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

    // @RpcMethod
    func setTextColor(_ color: String, respond: RPCMethodCallback) {
        if let textColor = UIColor(hex: color) {
            // Set the back button color
            navigationBar.tintColor = textColor

            // Set title text color
            // UINavigationBar has an annoying bug where it ignores
            // all appearance proxy titleTextAttributes if you set
            // any explicitly on an instance.
            var titleTextAttributes = UINavigationBar.appearance().titleTextAttributes ?? [NSAttributedString.Key: Any]()
            titleTextAttributes[NSAttributedString.Key.foregroundColor] = textColor
            navigationBar.titleTextAttributes = titleTextAttributes
        } else {
            respond(.error("Invalid hex color provided: '\(color)'."))
        }
    }

    // @RpcMethod
    func showBackButtonText(_ respond: RPCMethodCallback) {
        backButtonTextIsHidden = false

        updateBackButtonOfExistingNavigationItems()
    }

    // @RpcMethod
    func hideBackButtonText(_ respond: RPCMethodCallback) {
        backButtonTextIsHidden = true

        updateBackButtonOfExistingNavigationItems()
    }

    private func updateBackButtonOfExistingNavigationItems() {
        // Since it is not guaranteed that this method is called when initializing
        // the headerBarPlugin. We need to add the customized backBarItem
        // to all the navigationItems in the navigationBar
        if let navigationItems = navigationBar.items {
            for item in navigationItems {
                item.backBarButtonItem = createBackButton()
            }
        }
    }

    private func createBackButton() -> UIBarButtonItem? {
        // Setting the backBarButtonItem to nil will provide the default functionality of the back button
        // Default functionality adds the previous title of the page or the word 'Back' depending on
        // length of the title from the current page
        var backButton: UIBarButtonItem? = nil

        if backButtonTextIsHidden {
            backButton =  UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
        }

        return backButton
    }

    private func flipImage(_ image: UIImage) -> UIImage {
        return UIImage(cgImage: image.cgImage!, scale: image.scale, orientation: .upMirrored)
    }
}

// MARK: - HeaderContentCoordinator

extension HeaderBarPlugin: HeaderContentCoordinator {

    @objc func suspendUpdates() {
        updateQueue.isSuspended = true
    }

    @objc func resumeUpdates() {
        updateQueue.isSuspended = false
    }

    @objc func discardPendingUpdatesAndResume() {
        // Cancel updates and resume the queue so that the pending operations flush out
        if updateQueue.operationCount > 0 {
            updatesCancelled = true
        }
        updateQueue.isSuspended = false
    }

    func pushHeaderContent(_ headerContent: HeaderContent, coordinator: UIViewControllerTransitionCoordinator?) {
        var content = headerContent

        // Remove default header content when we first set up coordination using
        // the HeaderContentCoordinator protocol
        if !isCoordinating {
            isCoordinating = true
            // Grab any icons that got setup pre-coordination and set them on the new
            // headerContent object we are about to push.
            let preCoordinationHeaderContent = popHeaderContent(animated: false)
            if content != preCoordinationHeaderContent {
                if content.leftIcon == nil {
                    content.leftIcon = preCoordinationHeaderContent.leftIcon
                }
                if content.centerIcon == nil {
                    content.centerIcon = preCoordinationHeaderContent.centerIcon
                }
                if content.rightIcon == nil {
                    content.rightIcon = preCoordinationHeaderContent.rightIcon
                }
            }
        }

        if hasHeaderContent {
            self.pushHeaderContent(content, animated: true)
        } else {
            self.pushHeaderContent(content, animated: false)
        }
    }

    @objc func popHeaderContentWithCoordinator(_ coordinator: UIViewControllerTransitionCoordinator?, isSnapshot: Bool=false) {
        guard let coordinator = coordinator else {
            self.popHeaderContent(animated: true, isSnapshot: isSnapshot)
            return
        }

        var headerContent: HeaderContent?
        var cancelled = false

        coordinator.animateAlongsideTransition(in: viewController.view,
                                               animation: { context in

                                                // The popNavigationItemAnimated() animation doesn't complete when run
                                                // interactively. It progresses to the final state, but the completion
                                                // callback is never called. This callback is vital to the proper
                                                // function of the UINavigationController. The interface becomes
                                                // unresponsive because the navigation controller thinks the animation
                                                // is still in flight.
                                                //
                                                // Wrapping the built-in animation our own animateWithDuration() is a
                                                // workaround for this issue. The duration doesn't matter; the parent
                                                // transition's duration is used.

                                                UIView.animate(withDuration: 0.0, animations: {
                                                    headerContent = self.popHeaderContent(animated: true, isSnapshot: isSnapshot)
                                                })
                                            }, completion: { context in
                                                // If the interactive animation is cancelled we must push the popped
                                                // header content back onto the stack. No pop actually occurred. The
                                                // animation is cancelled when:
                                                //
                                                // 1. The user slowly swipes a short distance from the left edge and
                                                //    then lifts their finger.
                                                // 2. The user slides their finger back to the left edge.

                                                if cancelled {
                                                    if let headerContent = headerContent {
                                                        self.pushHeaderContent(headerContent, animated: false)
                                                    }
                                                }
                                            }
        )

        coordinator.notifyWhenInteractionEnds { context in
            // This is used above in the completion block of the
            // animateAlongsideTransitionInView:animation: call!
            // Don't erase this `cancelled` variable!
            cancelled = context.isCancelled
        }
    }

    @objc func popToRootHeaderContentWithCoordinator(_ coordinator: UIViewControllerTransitionCoordinator?) {
        if let coordinator = coordinator {
            // Pop nav items off
            coordinator.animateAlongsideTransition(in: viewController.view,
                                                   animation: { _ in
                                                    self.popToRootHeaderContent(animated: true)
            }, completion: nil)
        } else {
            self.popToRootHeaderContent(animated: false)
        }
    }

    @objc func removeHeaderContentAtIndex(_ index: Int) {
        if index == headerContentStack.count - 1 {
            // We want animation if we're removing the last item
            self.popHeaderContent(animated: true)
        } else if index <= headerContentStack.count {
            // When we pull an item out of the middle of the stack, we have to
            // ensure we keep the actual UINavigationBar items in sync
            headerContentStack.remove(at: index)
            navigationBar.items?.remove(at: index)
        }
    }

    func getLastHeaderContent() -> HeaderContent? {
        return headerContentStack.last
    }
}

// MARK: - HeaderBar view controller delegate

extension HeaderBarPlugin: HeaderBarViewControllerDelegate {
    @objc func triggerBackEvent() {
        trigger("click:back")
    }
}

// MARK: - Localization

extension HeaderBarPlugin: LocaleChangedListener {
    public func localeDidChange(newLocale: Locale) {
        navigationBar.setItems(self.navigationItems, animated: false)
    }
}
