//
//  AnchoredLayoutPlugin.swift
//  Astro
//
//  Created by Jeremy Wiebe on 2015-05-08.
//  Copyright (c) 2015 Mobify Research & Development Inc. All rights reserved.
//

import Foundation

public protocol ManagedContentInsets {
    func adjust(contentInsets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets)
}

public protocol ManagedContentOffsets {
    func setContentExtendsToBottomOfFrame()
}

class AnchoredLayoutViewController: UIViewController, ManagedContentViewState {
    private let defaultShowHideAnimationDuration = 0.4

    private let topContainerView = LinearLayoutView(direction: .down)
    private let bottomContainerView = LinearLayoutView(direction: .up)
    private let contentContainerView = AstroContainerView(frame: CGRect.zero)

    private var topViewControllers = [UIViewController]()
    private var bottomViewControllers = [UIViewController]()
    private var contentViewController: UIViewController? {
        willSet {
            if let contentViewController = contentViewController {
                removeChildViewControllers([contentViewController])
            }
        }
        didSet {
            if let contentViewController = contentViewController {
                addChildViewController(contentViewController, container: contentContainerView)
            }
        }
    }

    fileprivate var isTopViewHidden: Bool {
        return topContainerView.isHidden
    }

    fileprivate var isBottomViewHidden: Bool {
        return bottomContainerView.isHidden
    }

    @objc var anchorToLayoutGuides: Bool
    @objc var inlineContentViewConstraints: [NSLayoutConstraint]!

    @objc init(anchorToLayoutGuides: Bool) {
        self.anchorToLayoutGuides = anchorToLayoutGuides

        super.init(nibName: nil, bundle: nil)
        self.view.backgroundColor = UIColor.clear
    }

    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Order is important here. Content goes at the bottom.
        view.addSubview(contentContainerView)
        view.addSubview(topContainerView)
        view.addSubview(bottomContainerView)

        if #available(iOS 11.0, *) {
            topContainerView.constrainWithinSafeAreaOfSuperviewHorizontally()
            bottomContainerView.constrainWithinSafeAreaOfSuperviewHorizontally()
            contentContainerView.constrainWithinSafeAreaOfSuperviewHorizontally()

            // Determine whether we should anchor to the layout guides or to the super view.
            let topAnchorView = anchorToLayoutGuides ? view.safeAreaLayoutGuide.topAnchor : view.topAnchor
            let bottomAnchorView = anchorToLayoutGuides ? view.safeAreaLayoutGuide.bottomAnchor : view.bottomAnchor

            inlineContentViewConstraints = [
                topAnchorView.constraint(equalTo: topContainerView.topAnchor),
                topContainerView.bottomAnchor.constraint(equalTo: contentContainerView.topAnchor),
                contentContainerView.bottomAnchor.constraint(equalTo: bottomContainerView.topAnchor),
                bottomAnchorView.constraint(equalTo: bottomContainerView.bottomAnchor)
            ]

        } else {
            topContainerView.pinToSuperviewEdgesHorizontally()
            bottomContainerView.pinToSuperviewEdgesHorizontally()
            contentContainerView.pinToSuperviewEdgesHorizontally()

            let viewDictionary: [String: AnyObject] = [
                "topLayoutGuide": topLayoutGuide,
                "bottomLayoutGuide": bottomLayoutGuide,
                "topContainerView": topContainerView,
                "bottomContainerView": bottomContainerView,
                "contentContainerView": contentContainerView
            ]

            // Determine whether we should anchor to the layout guides or to the super view.
            let topAnchorView = anchorToLayoutGuides ? "[topLayoutGuide]" : "|"
            let bottomAnchorView = anchorToLayoutGuides ? "[bottomLayoutGuide]" : "|"

            // Init constraints.
            let visualFormatString = "V:\(topAnchorView)[topContainerView][contentContainerView][bottomContainerView]\(bottomAnchorView)"
            inlineContentViewConstraints = NSLayoutConstraint.constraints(withVisualFormat: visualFormatString, options: [], metrics: nil, views: viewDictionary)
        }

        updateConstraints()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        NotificationCenter.default.removeObserver(self)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
    }

    @objc func keyboardWillChangeFrame(_ sender: Notification) {
        if #available(iOS 11.0, *) {
            return
        }

        // Adjust scroll view insets to compensate for the keyboard.
        if contentViewController is ManagedContentInsets {
            if let userInfo = sender.userInfo, let keyboardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as AnyObject).cgRectValue {
                let keyboardFrame = contentContainerView.convert(keyboardFrame, from: nil)
                let keyboardContentInsets = UIEdgeInsets(
                    top: 0,
                    left: 0,
                    bottom: max(keyboardFrame.intersection(contentContainerView.bounds).height, 0),
                    right: 0
                )

                updateContentInsets(keyboardContentInsets)
            }
        }
    }

    @objc func keyboardWillHide() {
        if #available(iOS 11.0, *), let managedContentInsets = contentViewController as? ManagedContentOffsets {
            managedContentInsets.setContentExtendsToBottomOfFrame()
        }
        updateContentInsets()
    }

//     `overlayInsets` are insets for views that are obscuring parts of the
//     AnchoredLayoutPlugin but aren't in its view hierarchy. Right now the `overlayInsets`
//     only come from the keyboard window.
    @objc func updateContentInsets(_ overlayInsets: UIEdgeInsets = UIEdgeInsets.zero) {
        // Adjust scroll view insets to compensate for bottom container view.
        if let managedContentInsets = contentViewController as? ManagedContentInsets {
            let contentInsets = UIEdgeInsets(
                top: 0,
                left: 0,
                bottom: max(0, overlayInsets.bottom),
                right: 0
            )

            managedContentInsets.adjust(contentInsets: contentInsets, scrollIndicatorInsets: contentInsets)
        }
    }

    @objc func updateConstraints() {
        if !isViewLoaded {
            return
        }

        view.removeConstraints(inlineContentViewConstraints)
        view.addConstraints(inlineContentViewConstraints)
    }

    @objc func setContentViewController(_ viewController: UIViewController) {
        contentViewController = viewController

        viewController.view.pinToSuperviewEdges()
        updateConstraints()
        view.layoutIfNeeded()
        didGetAddedToContentView()
    }

    @objc func didGetAddedToContentView() {
        if let viewControllerToUpdate = contentViewController as? ManagedContentViewState {
            viewControllerToUpdate.didGetAddedToContentView()
        }
    }

    func addTopViewController(_ viewController: UIViewController, height: CGFloat?, visible: Bool, animated: Bool, animationDuration: TimeInterval?, respond: @escaping RPCMethodCallback) {
        addViewController(topContainerView, viewControllerList: &topViewControllers, viewController: viewController, height: height, visible: visible, animated: animated, animationDuration: animationDuration, respond: respond)
    }

    func addBottomViewController(_ viewController: UIViewController, height: CGFloat?, visible: Bool, animated: Bool, animationDuration: TimeInterval?, respond: @escaping RPCMethodCallback) {
        addViewController(bottomContainerView, viewControllerList: &bottomViewControllers, viewController: viewController, height: height, visible: visible, animated: animated, animationDuration: animationDuration, respond: respond)
    }

    func addViewController(_ containerView: LinearLayoutView, viewControllerList: inout [UIViewController], viewController: UIViewController, height: CGFloat?, visible: Bool, animated: Bool, animationDuration: TimeInterval?, respond: @escaping RPCMethodCallback) {
        let animationDuration = self.animationDuration(animated: animated, animationDuration: animationDuration)

        addChildViewController(viewController, container: containerView, height: height)
        viewControllerList.append(viewController)

        // hiding the subview so that we can animate showing it
        // calling the hideSubview methods sets the state of the view so that it is 
        // in the correct state when calling showSubview
        containerView.hideSubview(viewController.view)
        if visible {
            containerView.showSubview(viewController.view,
                                      animated: animated,
                                      animationDuration: animationDuration,
                                      respond: respond)
        }
    }

    @objc func clearContentView() {
        contentViewController?.view.removeFromSuperview()
        contentViewController = nil
    }

    @objc func clearTopViews() {
        topContainerView.removeAllSubviews()
        removeChildViewControllers(topViewControllers)
        topViewControllers = []

        if isTopViewHidden {
            if let positionConstraint = topContainerView.findPositioningConstraint(inParent: view, direction: topContainerView.direction) {
                positionConstraint.constant = 0
            }
        }
    }

    @objc func clearBottomViews() {
        bottomContainerView.removeAllSubviews()
        removeChildViewControllers(bottomViewControllers)
        bottomViewControllers = []

        if isBottomViewHidden {
            if let positionConstraint = bottomContainerView.findPositioningConstraint(inParent: view, direction: bottomContainerView.direction) {
                positionConstraint.constant = 0
            }
        }
    }

    func addChildViewController(_ childViewController: UIViewController, container containerView: UIView, height: CGFloat? = nil) {
        childViewController.view.translatesAutoresizingMaskIntoConstraints = false

        addChild(childViewController)
        childViewController.didMove(toParent: self)

        containerView.addSubview(childViewController.view)

        if let height = height {
            childViewController.view.constrainToHeight(height)
        }

        if topContainerView.isHidden {
            updateConstraintsForHiddenContainerView(topContainerView)
        }
        if bottomContainerView.isHidden {
            updateConstraintsForHiddenContainerView(bottomContainerView)
        }

        view.setNeedsLayout()
    }

    @objc func removeChildViewControllers(_ viewControllers: [UIViewController]) {
        for viewController in viewControllers {
            viewController.view.removeFromSuperview()

            viewController.willMove(toParent: nil)
            viewController.removeFromParent()
        }

        if topContainerView.isHidden {
            updateConstraintsForHiddenContainerView(topContainerView)
        }
        if bottomContainerView.isHidden {
            updateConstraintsForHiddenContainerView(bottomContainerView)
        }
    }

    // MARK: - Animations

    private func animationDuration(animated: Bool, animationDuration: TimeInterval?) -> TimeInterval {
        return animated ? animationDuration ?? defaultShowHideAnimationDuration : 0
    }

    func toggleView(_ plugin: ViewPlugin, animated: Bool, animationDuration: TimeInterval?, respond: @escaping RPCMethodCallback) {
        let animationDuration = self.animationDuration(animated: animated, animationDuration: animationDuration)

        UIView.animate(withDuration: animationDuration,
            animations: {
                // Just try toggling the view in top and bottom container.
                // The container will just ignore calls for plugins it doesn't contain.
                self.topContainerView.toggleSubview(plugin.viewController.view, animated: animated, animationDuration: animationDuration)
                self.bottomContainerView.toggleSubview(plugin.viewController.view, animated: animated, animationDuration: animationDuration)

                // Ensure constraints for the content view are animated.
                self.view.layoutIfNeeded()
            },
            completion: { _ in
                respond(RPCMethodResult.result("Animation complete"))
            }
        )
    }

    func showView(_ plugin: ViewPlugin, animated: Bool, animationDuration: TimeInterval?, respond: @escaping RPCMethodCallback) {
        let animationDuration = self.animationDuration(animated: animated, animationDuration: animationDuration)

        UIView.animate(withDuration: animationDuration,
            animations: {
                // Just try showing the view in top and bottom container.
                // The container will just ignore calls for plugins it doesn't contain.
                self.topContainerView.showSubview(plugin.viewController.view, animated: animated, animationDuration: animationDuration, respond: nil)
                self.bottomContainerView.showSubview(plugin.viewController.view, animated: animated, animationDuration: animationDuration, respond: nil)

                // Ensure constraints for the content view are animated.
                self.view.layoutIfNeeded()
            },
            completion: { _ in
                respond(RPCMethodResult.result("Animation complete"))
            }
        )
    }

    func hideView(_ plugin: ViewPlugin, animated: Bool, animationDuration: TimeInterval?, respond: @escaping RPCMethodCallback) {
        let animationDuration = self.animationDuration(animated: animated, animationDuration: animationDuration)

        UIView.animate(withDuration: animationDuration,
            animations: {
                // Just try hiding the view in top and bottom container.
                // The container will just ignore calls for plugins it doesn't contain.
                self.topContainerView.hideSubview(plugin.viewController.view, animated: animated, animationDuration: animationDuration)
                self.bottomContainerView.hideSubview(plugin.viewController.view, animated: animated, animationDuration: animationDuration)

                // Ensure constraints for the content view are animated.
                self.view.layoutIfNeeded()
            },
            completion: { _ in
                respond(RPCMethodResult.result("Animation complete"))
            }
        )
    }

    func showTopViews(animated: Bool, animationDuration: TimeInterval?, respond: @escaping RPCMethodCallback) {
        showTopViews()
        animateLayout(animated, animationDuration: animationDuration, respond: respond)
    }

    @objc func showTopViews() {
        if !topContainerView.isHidden {
            return
        }

        if let positionConstraint = topContainerView.findPositioningConstraint(inParent: view, direction: topContainerView.direction) {
            positionConstraint.constant = 0
            topContainerView.isHidden = false
        }
    }

    func hideTopViews(animated: Bool, animationDuration: TimeInterval?, completion: (() -> Void)?, respond: @escaping RPCMethodCallback) {
        if topContainerView.isHidden {
            return
        }

        if updateConstraintsForHiddenContainerView(topContainerView) {
            let animationDuration = self.animationDuration(animated: animated, animationDuration: animationDuration)

            UIView.animate(withDuration: animationDuration,
                animations: {
                    self.view.layoutIfNeeded()
                },
                completion: { _ in
                    self.topContainerView.isHidden = true
                    completion?()
                    respond(RPCMethodResult.result("Animation complete"))
                }
            )
        }
    }

    func showBottomViews(animated: Bool, animationDuration: TimeInterval?, respond: @escaping RPCMethodCallback) {
        showBottomViews()
        animateLayout(animated, animationDuration: animationDuration, respond: respond)
    }

    func animateLayout(_ animated: Bool, animationDuration: TimeInterval?, respond: @escaping RPCMethodCallback) {
        let animationDuration = self.animationDuration(animated: animated, animationDuration: animationDuration)

        UIView.animate(withDuration: animationDuration,
            animations: {
                self.view.layoutIfNeeded()
            },
            completion: { _ in
                respond(RPCMethodResult.result("Animation complete"))
            }
        )
    }

    @objc func showBottomViews() {
        if !bottomContainerView.isHidden {
            return
        }

        if let positionConstraint = bottomContainerView.findPositioningConstraint(inParent: view, direction: bottomContainerView.direction) {
            positionConstraint.constant = 0
            bottomContainerView.isHidden = false
        }
    }

    func hideBottomViews(animated: Bool, animationDuration: TimeInterval?, completion: (() -> Void)?, respond: @escaping RPCMethodCallback) {
        if bottomContainerView.isHidden {
            return
        }

        if updateConstraintsForHiddenContainerView(bottomContainerView) {
            let animationDuration = self.animationDuration(animated: animated, animationDuration: animationDuration)

            UIView.animate(withDuration: animationDuration,
                animations: {
                    self.view.layoutIfNeeded()
                },
                completion: { _ in
                    self.bottomContainerView.isHidden = true
                    completion?()
                    respond(RPCMethodResult.result("Animation complete"))
                }
            )
        }
    }

    @objc @discardableResult
    func updateConstraintsForHiddenContainerView(_ containerView: LinearLayoutView) -> Bool {
        if let positionConstraint = containerView.findPositioningConstraint(inParent: view, direction: containerView.direction) {
            self.view.layoutIfNeeded()

            var offset = containerView.frame.height

            // Special case for top container and status bar.
            // We want the constraint to be at 0 if the top container is
            // attached to the topLayoutGuide so that our main content
            // container's inset is not 20px down (status bar height) as 
            // our content insets are calculated based on the placement
            // of the top and bottom containers.
            //
            // Note: Will never be applicable for ios 11+
            // Similar implementation might be necessary for ios 11+ but no good way to repro on sandbox or scaffold
            // These issues appeared on Extra so if updated to iphone X might need to investigate at that point
            if positionConstraint.firstItem === topLayoutGuide || positionConstraint.secondItem === topLayoutGuide {
                offset += 20
            }

            // for ios 11 + this needs to be +ve for top and -ve for bottom
            if #available(iOS 11.0, *), containerView.direction == Astro.UIViewLayoutDirection.down {
                // Do nothing as offset should be positive in this case
                // Note: #notavailable is not a thing in swift.
                // That is why the if statement only sets a value in the else
            } else {
                offset = -offset
            }

            positionConstraint.constant = offset
            return true
        }
        return false
    }
}

class AnchoredLayoutPlugin: Plugin, ViewPlugin {
    @objc var typedViewController: AnchoredLayoutViewController
    @objc var viewController: UIViewController {
        return typedViewController
    }

    required init(address: MessageAddress, messageBus: MessageBus, pluginResolver: PluginResolver, options: JSONObject?) {
        let anchorToLayoutGuides = options?["anchorToLayoutGuides"] as? Bool ?? true
        typedViewController = AnchoredLayoutViewController(anchorToLayoutGuides: anchorToLayoutGuides)

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

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

        addAsyncRpcMethodShim("addTopView") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let address: MessageAddress = MethodShimUtils.getArg(params, key: "address", respond: respond) {
                if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
                    self.addTopView(address, options: options, respond: respond)
                }
            }
            /////////////////////////////////////////////////////////////
        }

        addAsyncRpcMethodShim("addBottomView") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let address: MessageAddress = MethodShimUtils.getArg(params, key: "address", respond: respond) {
                if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
                    self.addBottomView(address, options: options, respond: respond)
                }
            }
            /////////////////////////////////////////////////////////////
        }

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

        addAsyncRpcMethodShim("clearTopViews") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
                self.clearTopViews(options: options, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        addAsyncRpcMethodShim("clearBottomViews") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
                self.clearBottomViews(options: options, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        addAsyncRpcMethodShim("toggleView") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let address: String = MethodShimUtils.getArg(params, key: "address", respond: respond) {
                if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
                    self.toggleView(at: address, options: options, respond: respond)
                }
            }
            /////////////////////////////////////////////////////////////
        }

        addAsyncRpcMethodShim("showView") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let address: String = MethodShimUtils.getArg(params, key: "address", respond: respond) {
                if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
                    self.showView(at: address, options: options, respond: respond)
                }
            }
            /////////////////////////////////////////////////////////////
        }

        addAsyncRpcMethodShim("hideView") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let address: String = MethodShimUtils.getArg(params, key: "address", respond: respond) {
                if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
                    self.hideView(at: address, options: options, respond: respond)
                }
            }
            /////////////////////////////////////////////////////////////
        }

        addAsyncRpcMethodShim("showTopViews") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
                self.showTopViews(options: options, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        addAsyncRpcMethodShim("hideTopViews") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
                self.hideTopViews(options: options, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        addAsyncRpcMethodShim("showBottomViews") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
                self.showBottomViews(options: options, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        addAsyncRpcMethodShim("hideBottomViews") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
                self.hideBottomViews(options: options, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }
    }

    // @RpcMethod
    func setContentView(_ address: MessageAddress, respond: RPCMethodCallback) {
        if let plugin: ViewPlugin = pluginResolver.pluginInstanceByAddress(address, respond: respond) {
            typedViewController.setContentViewController(plugin.viewController)
        }
    }

    // @RpcMethod
    func addTopView(_ address: MessageAddress, options: JSONObject?, respond: @escaping RPCMethodCallback) {
        if let plugin: ViewPlugin = pluginResolver.pluginInstanceByAddress(address, respond: respond) {
            typedViewController.addTopViewController(plugin.viewController,
                                                     height: options?["height"] as? CGFloat,
                                                     visible: options?["visible"] as? Bool ?? true,
                                                     animated: animatedFromOptions(options),
                                                     animationDuration: animationDurationFromOptions(options),
                                                     respond: respond)
        }
    }

    // @RpcMethod
    func addBottomView(_ address: MessageAddress, options: JSONObject?, respond: @escaping RPCMethodCallback) {
        if let plugin: ViewPlugin = pluginResolver.pluginInstanceByAddress(address, respond: respond) {
            typedViewController.addBottomViewController(plugin.viewController,
                                                        height: options?["height"] as? CGFloat,
                                                        visible: options?["visible"] as? Bool ?? true,
                                                        animated: animatedFromOptions(options),
                                                        animationDuration: animationDurationFromOptions(options),
                                                        respond: respond)
        }
    }

    // @RpcMethod
    func clearContentView(respond: RPCMethodCallback) {
        typedViewController.clearContentView()
    }

    // @RpcMethod
    func clearTopViews(options: JSONObject?, respond: @escaping RPCMethodCallback) {
        // Top views are hidden no animation needed when clearing them
        if typedViewController.isTopViewHidden {
            typedViewController.clearTopViews()
            respond(RPCMethodResult.result("Clear top views complete"))
            return
        }

        // Animate hiding the top views
        // Once the animation is complete the top views are cleared
        // Re-show the empty top views. This ensures the container is in the correct state
        // when new new views are added
        typedViewController.hideTopViews(animated: animatedFromOptions(options), animationDuration: animationDurationFromOptions(options), completion: {
            self.typedViewController.clearTopViews()
            self.typedViewController.showTopViews()
        }, respond: respond)
    }

    // @RpcMethod
    func clearBottomViews(options: JSONObject?, respond: @escaping RPCMethodCallback) {
        // Bottom views are hidden no animation needed when clearing them
        if typedViewController.isBottomViewHidden {
            typedViewController.clearBottomViews()
            respond(RPCMethodResult.result("Clear bottom views complete"))
            return
        }

        // Animate hiding the bottom views
        // Once the animation is complete the bottom views are cleared
        // Re-show the empty bottom views. This ensures the container is in the correct state
        // when new new views are added
        typedViewController.hideBottomViews(animated: animatedFromOptions(options), animationDuration: animationDurationFromOptions(options), completion: {
            self.typedViewController.clearBottomViews()
            self.typedViewController.showBottomViews()
        }, respond: respond)
    }

    // @RpcMethod
    func toggleView(at address: String, options: JSONObject?, respond: @escaping RPCMethodCallback) {
        if let plugin: ViewPlugin = pluginResolver.pluginInstanceByAddress(address, respond: respond) {
            typedViewController.toggleView(plugin, animated: animatedFromOptions(options), animationDuration: animationDurationFromOptions(options), respond: respond)
        }
    }

    // @RpcMethod
    func showView(at address: String, options: JSONObject?, respond: @escaping RPCMethodCallback) {
        if let plugin: ViewPlugin = pluginResolver.pluginInstanceByAddress(address, respond: respond) {
            typedViewController.showView(plugin, animated: animatedFromOptions(options), animationDuration: animationDurationFromOptions(options), respond: respond)
        }
    }

    // @RpcMethod
    func hideView(at address: String, options: JSONObject?, respond: @escaping RPCMethodCallback) {
        if let plugin: ViewPlugin = pluginResolver.pluginInstanceByAddress(address, respond: respond) {
            typedViewController.hideView(plugin, animated: animatedFromOptions(options), animationDuration: animationDurationFromOptions(options), respond: respond)
        }
    }

    // @RpcMethod
    func showTopViews(options: JSONObject?, respond: @escaping RPCMethodCallback) {
        typedViewController.showTopViews(animated: animatedFromOptions(options), animationDuration: animationDurationFromOptions(options), respond: respond)
    }

    // @RpcMethod
    func hideTopViews(options: JSONObject?, respond: @escaping RPCMethodCallback) {
        typedViewController.hideTopViews(animated: animatedFromOptions(options), animationDuration: animationDurationFromOptions(options), completion: nil, respond: respond)
    }

    // @RpcMethod
    func showBottomViews(options: JSONObject?, respond: @escaping RPCMethodCallback) {
        typedViewController.showBottomViews(animated: animatedFromOptions(options), animationDuration: animationDurationFromOptions(options), respond: respond)
    }

    // @RpcMethod
    func hideBottomViews(options: JSONObject?, respond: @escaping RPCMethodCallback) {
        typedViewController.hideBottomViews(animated: animatedFromOptions(options), animationDuration: animationDurationFromOptions(options), completion: nil, respond: respond)
    }

    private func animatedFromOptions(_ options: JSONObject?) -> Bool {
        return options?["animated"] as? Bool ?? false
    }

    private func animationDurationFromOptions(_ options: JSONObject?) -> TimeInterval? {
        if let animationDurationInMilliseconds = options?["animationDuration"] as? Int {
            return TimeInterval(animationDurationInMilliseconds) / 1000
        }
        return nil
    }
}
