//
//  UIPersistingNavigationController.swift
//  Astro
//
//  Created by Shawn Jansepar on 2015-06-09.
//  Copyright (c) 2015 Mobify Research & Development Inc. All rights reserved.
//

import WebKit

protocol PersistingNavigationControllerDelegate: class {
    func pop(from source: UIViewController?, to destination: UIViewController?)
    func popToRoot(poppedControllers: [UIViewController]?, root: UIViewController?)
    func didPopToViewController(_ viewController: UIViewController)
    func didPushToViewController(_ viewController: UIViewController)
    func didPopToRootViewController(_ viewController: UIViewController)
}

private enum TransitionType {
    case push
    case pop
    case popToRoot
    case none
}

public class PersistingNavigationController: UINavigationController, ManagedContentInsets, ManagedContentOffsets, ManagedContentViewState {

    // The number of web views at the head and tail of the navigationController.viewControllers
    // array to keep "hydrated"   head --> [vroot][v1 - stored]....[v6 - stored][v7][v8][v9][vcurrent] <-- tail
    @objc static let maxHeadViews = 1
    @objc static let maxTailViews = 4
    @objc static var maxViews: Int {
        return maxHeadViews + maxTailViews
    }

    weak var persistingDelegate: PersistingNavigationControllerDelegate?
    weak var headerContentCoordinator: HeaderContentCoordinator?

    // The UINavigationController provides a 'interactivePopNavigationGesture' which allows
    // the user to swipe from the left edge to do a pop navigation. When this happens the
    // UINavigationController calls popViewControllerAnimated() but does not call anything
    // if the user aborts the swipe.
    // It also calls navigationController:willShowViewController:animated: and
    // navigationController:didShowViewController:animated: whenever a push or pop navigation
    // occurs. The view controller is the one that became the new top-most view controller.
    // In these methods we coordinate with the linked HeaderBarPlugin to tell it to push
    // or pop header content. We only want to do this while in a navigation so we track
    // that using these two flags otherwise we tell the linked HeaderBarPlugin to pop
    // header content whenever the topmost view controller becomes visible (which also
    // happens when this navigation controller is added back into the view hierarchy).
    fileprivate var transitionType: TransitionType = .none

    // A UINavigationController pairs `willShowViewController` and `didShowViewController`
    // delegate calls when it is pushing and popping view controllers onto it's stack.
    // Unfortunately it also calls this pair of methods when it is being revealed
    // through other means such as being covered by a modal or swapped out of the view
    // hierarchy and back in. Sometimes, what happens is the `willShowViewController` is
    // called but not the `didShowViewController`. The `PersistingNavigationController`
    // depends heavily on balanced calls to `willShow` and `didShow`. This flag tracks
    // whether both have been called, and ignores subsequent, unbalanced
    // `willShowViewController` calls until a matched `didShowViewController` is called.
    fileprivate var isPendingDidShowViewControllerCall = false

    fileprivate var currentContentInsets: UIEdgeInsets?
    fileprivate var currentScrollIndicatorInsets: UIEdgeInsets?

    // Unfortunately since we are inserting a new placeholder view controller in the
    // array of view controllers during the pwa navigation the willShow function
    // returns the placeholder view controller and the web view controller that is
    // being moved to the top of the stack is not accessible at the time that willShow is called.
    // Thus a boolean to know if we are pwa Navigating and a variable to save the new header
    // content are added here and accessed during didShow
    var pwaHeaderContent: HeaderContent?
    var inPwaNavigation = false

    init() {
        super.init(nibName: nil, bundle: nil)

        self.delegate = self
    }

    @available(*, unavailable, message: "init(coder:) is unavailable")
    required public init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private static func adjustViewControllerContentInsets(_ viewController: AnyObject, contentInsets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets) {
        if let viewController = viewController as? ManagedContentInsets {
            viewController.adjust(contentInsets: contentInsets, scrollIndicatorInsets: scrollIndicatorInsets)
        }
    }

    public func adjust(contentInsets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets) {
        self.currentContentInsets = contentInsets
        self.currentScrollIndicatorInsets = scrollIndicatorInsets
        if let contentInsets = self.currentContentInsets, let scrollIndicatorInsets = self.currentScrollIndicatorInsets {
            for viewController in viewControllers {
                PersistingNavigationController.adjustViewControllerContentInsets(viewController, contentInsets: contentInsets, scrollIndicatorInsets: scrollIndicatorInsets)
            }
        }
    }

    public func setContentExtendsToBottomOfFrame() {
        if let viewController = viewControllers.last as? ManagedContentOffsets {
            viewController.setContentExtendsToBottomOfFrame()
        }
    }

    @objc public func didGetAddedToContentView() {
        for viewController in viewControllers {
            if let viewController = viewController as? ManagedContentViewState {
                viewController.didGetAddedToContentView()
            }
        }
    }

    public func popToRoot(animated: Bool) {
        // Created this intermidiary method because we need to wait for any transitions to complete before calling popToRootViewController
        // Decided to put this logic inside the PersistingNavigationController
        guard let coordinator = transitionCoordinator else {
            let _ = popToRootViewController(animated: animated)
            return
        }

        coordinator.animateAlongsideTransition(in: self.view,
                                               animation: nil,
                                               completion: { _ in
                                                let _ = self.popToRootViewController(animated: animated)
        })
    }

    public override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        self.transitionType = .push

        // This should have a [weak self], but isn't supported by Swift yet because
        // we're calling `super` inside this block
        let push = {
            if let contentInsets = self.currentContentInsets, let scrollIndicatorInsets = self.currentScrollIndicatorInsets {
                PersistingNavigationController.adjustViewControllerContentInsets(viewController, contentInsets: contentInsets, scrollIndicatorInsets: scrollIndicatorInsets)
            }

            super.pushViewController(viewController, animated: animated)

            if self.viewControllers.count > PersistingNavigationController.maxViews {
                // Remove the last recently used view controller that isn't a head view controller
                // (the head view controllers are likely one that we want to always keep around for
                // fast back-to-root purposes).
                let indexOfViewToBeRemoved = self.viewControllers.count - PersistingNavigationController.maxTailViews - 1

                // Grab a reference to the last view controller and persist it
                if let persistableViewController = self.viewControllers[indexOfViewToBeRemoved] as? Persistable {
                    AstroLog.logger(AstroLog.Application).debug("Saving state for \(indexOfViewToBeRemoved)")
                    persistableViewController.save()
                }
            }
        }

        // This is ensures that we don't start a new transition while another one is in progress.
        // If we don't do this, the new view controller we push will not be visible and
        // the following message will be logged: pushViewController:animated: called on
        // <Astro.PersistingNavigationController 0x23232> while an existing transition or
        // presentation  is occurring; the navigation stack will not be updated.
        if let t = transitionCoordinator {
            t.animateAlongsideTransition(in: self.view,
                                         animation: nil,
                                         completion: { _ in
                                            push()
            })
        } else {
            push()
        }
    }

    public override func popViewController(animated: Bool) -> UIViewController? {
        self.transitionType = .pop

        headerContentCoordinator?.suspendUpdates()

        let poppedViewController = super.popViewController(animated: animated)

        // If no transitionCoordinator we can short circuit as there's no
        // asynchronous operation in play
        guard let transitionCoordinator = transitionCoordinator else {
            persistingDelegate?.pop(from: poppedViewController, to: self.viewControllers.last)
            return poppedViewController
        }

        transitionCoordinator.notifyWhenInteractionEnds { [weak self] context in
            // When the pop is interactive, if the user cancels
            // the operation, we don't want to pop and need to tell the
            // header to resume operations
            if context.isCancelled {
                self?.headerContentCoordinator?.discardPendingUpdatesAndResume()
                self?.transitionType = .none
            }
        }

        // We only want to inform that a pop occurred when the animation has
        // completed
        transitionCoordinator.animate(alongsideTransition: nil, completion: { [weak self] context in
            if !context.isCancelled {
                self?.persistingDelegate?.pop(from: poppedViewController,
                                              to: self?.viewControllers.last)
            }
        })

        return poppedViewController
    }

    public override func popToRootViewController(animated: Bool) -> [UIViewController]? {
        if viewControllers.count > 1 {
            self.transitionType = .popToRoot
        }
        let poppedViewControllers = super.popToRootViewController(animated: false)
        persistingDelegate?.popToRoot(poppedControllers: poppedViewControllers, root: self.topViewController)

        return poppedViewControllers
    }

    // Remove a given view controller from anywhere in the stack
    @objc func removeViewController(_ viewController: UIViewController) {
        if let indexToRemove = viewControllers.firstIndex(of: viewController) {
            viewControllers.remove(at: indexToRemove)
            headerContentCoordinator?.removeHeaderContentAtIndex(indexToRemove)
        }
    }
}

// MARK: - UINavigationControllerDelegate

extension PersistingNavigationController: UINavigationControllerDelegate {
    public func navigationController(_ navigationController: UINavigationController,
                                     willShow viewController: UIViewController,
                                     animated: Bool) {

        guard !isPendingDidShowViewControllerCall else {
            return
        }

        let coordinator = transitionCoordinator
        switch self.transitionType {
        case .popToRoot:
            headerContentCoordinator?.popToRootHeaderContentWithCoordinator(coordinator)
        case .pop:
            // This view controller is the same as the destination in didPop
            headerContentCoordinator?.popHeaderContentWithCoordinator(coordinator, isSnapshot: viewController is PlaceholderViewController)
        case .push:
            var content: HeaderContent?

            if (self.inPwaNavigation) {
                if let pwaHeaderContent = pwaHeaderContent {
                    content = pwaHeaderContent
                }
            }
            // check if theres an existing headercontent from the viewController, else it gets firstHeaderContent
            // if there isn't any headerContent present then create an empty one
            content = content ?? viewController.headerContent ?? headerContentCoordinator?.getLastHeaderContent()
            headerContentCoordinator?.pushHeaderContent(content ?? HeaderContent(), coordinator: coordinator)
        case .none:
            return
        }

        // Track if we have a pending didShow: call and revert it if the
        // current navigation is cancelled because our cleanup code in
        // didShowViewController: never gets called when the interactive
        // pop navigation is cancelled!
        isPendingDidShowViewControllerCall = true
        if let coordinator = coordinator {
            coordinator.notifyWhenInteractionEnds { [weak self] context in
                if context.isCancelled {
                    self?.isPendingDidShowViewControllerCall = false
                    self?.transitionType = .none
                }
            }
        }
    }

    public func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        switch self.transitionType {
        case .popToRoot:
            headerContentCoordinator?.discardPendingUpdatesAndResume()
            persistingDelegate?.didPopToRootViewController(viewController)
        case .pop:
            headerContentCoordinator?.discardPendingUpdatesAndResume()
            // Don't need to restore because all our view controllers have already been restored (maxTailViews).
            guard viewControllers.count >= PersistingNavigationController.maxTailViews else {
                break
            }

            let viewControllerIndex = viewControllers.count - PersistingNavigationController.maxTailViews
            if let persistableViewController = viewControllers[viewControllerIndex] as? Persistable {
                persistableViewController.restore(contentInsets: self.currentContentInsets, scrollIndicatorInsets: self.currentScrollIndicatorInsets)
            }
            persistingDelegate?.didPopToViewController(viewController)
        case .push:
            // viewController not the correct one for PWA navigations
            persistingDelegate?.didPushToViewController(viewController)
        case .none:
            return
        }

        self.transitionType = .none
        isPendingDidShowViewControllerCall = false
        self.inPwaNavigation = false
        self.pwaHeaderContent = nil
    }
}
