//
//  MultiColumnNavigationController.swift
//  Tusker
//
//  Created by Shadowfacts on 8/24/23.
//  Copyright © 2023 Shadowfacts. All rights reserved.
//

import UIKit

class MultiColumnNavigationController: UIViewController {
    
    private var isManuallyUpdating = false
    var viewControllers: [UIViewController]  = [] {
        didSet {
            guard isViewLoaded,
                  !isManuallyUpdating else {
                return
            }
            updateViews()
            scrollToEnd(animated: false)
        }
    }
    
    private var scrollView = UIScrollView()
    private var stackView = UIStackView()
    
    init() {
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .appSecondaryBackground
        
        scrollView.contentInsetAdjustmentBehavior = .always
        scrollView.contentInset = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
        scrollView.alwaysBounceHorizontal = true
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        
        stackView.axis = .horizontal
        stackView.spacing = 8
        stackView.alignment = .fill
        stackView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(stackView)
        
        view.addSubview(scrollView)
        NSLayoutConstraint.activate([
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            
            stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
            stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
            stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
            stackView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor),
        ])
        
        updateViews()
    }
    
    private func updateViews() {
        var i = 0
        while i < viewControllers.count {
            let needsCloseButton = i > 0
            if i <= stackView.arrangedSubviews.count - 1 {
                let existing = stackView.arrangedSubviews[i] as! ColumnView
                existing.setContent(viewControllers[i], needsCloseButton: needsCloseButton)
            } else {
                let new = ColumnView(owner: self, contentViewController: viewControllers[i], needsCloseButton: needsCloseButton)
                stackView.addArrangedSubview(new)
            }
            i += 1
        }
        
        while i < stackView.arrangedSubviews.count {
            let toRemove = stackView.arrangedSubviews[i] as! ColumnView
            toRemove.willRemoveColumn()
            stackView.removeArrangedSubview(toRemove)
        }
    }
    
    override func show(_ vc: UIViewController, sender: Any?) {
        if let sender = sender as? UIViewController {
            var index: Int? = nil
            var current: UIViewController? = sender
            while let c = current {
                index = viewControllers.firstIndex(of: c)
                if index != nil {
                    break
                } else {
                    current = c.parent
                }
            }
            if let index {
                replaceViewControllers([vc], after: index, animated: true)
            } else {
                pushViewController(vc, animated: true)
            }
        } else {
            pushViewController(vc, animated: true)
        }
        
        SplitNavigationController.clearSelectedRow(sender: sender)
    }
    
    func replaceViewControllers(_ vcs: [UIViewController], after afterIndex: Int, animated: Bool) {
        if afterIndex == viewControllers.count - 1 && vcs.count == 1 {
            pushViewController(vcs[0], animated: animated)
        } else {
            viewControllers = Array(viewControllers[...afterIndex]) + vcs
            scrollToEnd(animated: animated)
        }
    }
    
    private func scrollToEnd(animated: Bool) {
        if viewControllers.isEmpty {
            scrollView.setContentOffset(.init(x: -scrollView.adjustedLeadingContentInset, y: -scrollView.adjustedContentInset.top), animated: false)
        } else {
            scrollColumnToEnd(columnIndex: viewControllers.count - 1, animated: animated)
        }
    }
    
    private func scrollColumnToEnd(columnIndex: Int, animated: Bool) {
        scrollView.layoutIfNeeded()
        let column = stackView.arrangedSubviews[columnIndex]
        let columnFrame = column.convert(column.bounds, to: scrollView)
        let offset: CGFloat
        if columnFrame.maxX < scrollView.bounds.width - scrollView.adjustedTrailingContentInset {
            offset = -scrollView.adjustedLeadingContentInset
        } else {
            offset = columnFrame.minX + scrollView.adjustedLeadingContentInset - (scrollView.bounds.width - columnFrame.width)
        }
        scrollView.setContentOffset(CGPoint(x: offset, y: -scrollView.adjustedContentInset.top), animated: animated)
    }
    
    fileprivate func closeColumn(_ vc: UIViewController) {
        let index = viewControllers.firstIndex(of: vc)!
        guard index > 0 else {
            // Can't close the last column
            return
        }
        isManuallyUpdating = true
        defer { isManuallyUpdating = false }
        viewControllers.removeSubrange(index...)
        animateChanges {
            for column in self.stackView.arrangedSubviews[index...] {
                column.layer.opacity = 0
            }
            self.scrollColumnToEnd(columnIndex: index - 1, animated: false)
        } completion: {
            self.updateViews()
        }

    }
    
    private func animateChanges(_ animations: @escaping () -> Void, completion: (() -> Void)? = nil) {
        let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters())
        animator.addAnimations(animations)
        animator.addCompletion { _ in
            completion?()
        }
        animator.startAnimation()
    }
}

extension MultiColumnNavigationController: NavigationControllerProtocol {
    var topViewController: UIViewController? {
        viewControllers.last
    }
    
    func popToRootViewController(animated: Bool) -> [UIViewController]? {
        let removed = Array(viewControllers.dropFirst())
        viewControllers = [viewControllers.first!]
        return removed
    }
    
    func pushViewController(_ vc: UIViewController, animated: Bool) {
        isManuallyUpdating = true
        defer { isManuallyUpdating = false }
        viewControllers.append(vc)
        updateViews()
        scrollToEnd(animated: animated)
        if animated {
            let column = stackView.arrangedSubviews.last!
            column.layer.opacity = 0
            animateChanges {
                column.layer.opacity = 1
            }
        }
    }
}

private class ColumnView: UIView {
    private unowned let owner: MultiColumnNavigationController
    private let contentView = UIView()
    private let navigationController: ColumnNavigationController
    private var contentViewController: UIViewController!
    
    init(owner: MultiColumnNavigationController, contentViewController: UIViewController, needsCloseButton: Bool) {
        self.owner = owner
        self.navigationController = ColumnNavigationController(owner: owner)
        super.init(frame: .zero)
        
        contentView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(contentView)
        
        layer.shadowOpacity = 0.2
        layer.shadowRadius = 8
        layer.shadowOffset = .zero
        contentView.layer.masksToBounds = false
        contentView.layer.cornerRadius = 12.5
        contentView.layer.cornerCurve = .continuous
        contentView.layer.masksToBounds = true
        
        NSLayoutConstraint.activate([
            contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
            contentView.topAnchor.constraint(equalTo: topAnchor),
            contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
            
            widthAnchor.constraint(equalToConstant: 400),
        ])
        
        setContent(contentViewController, needsCloseButton: needsCloseButton)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setContent(_ viewController: UIViewController, needsCloseButton: Bool) {
        guard viewController !== contentViewController || viewController.parent !== navigationController else {
            return
        }
        
        contentViewController?.removeViewAndController()
        
        if navigationController.parent != owner {
            navigationController.removeViewAndController()
            
            owner.addChild(navigationController)
            navigationController.didMove(toParent: owner)
            navigationController.view.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(navigationController.view)
            NSLayoutConstraint.activate([
                navigationController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
                navigationController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
                navigationController.view.topAnchor.constraint(equalTo: contentView.topAnchor),
                navigationController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            ])
        }
        
        contentViewController = viewController
        navigationController.setViewControllers([viewController], animated: false)
        
        if needsCloseButton {
            installCloseBarButton(navigationItem: viewController.navigationItem)
        }
    }
    
    func willRemoveColumn() {
        navigationController.removeViewAndController()
        navigationController.setViewControllers([], animated: false)
        removeCloseBarButton(navigationItem: contentViewController.navigationItem)
    }
    
    private func installCloseBarButton(navigationItem: UINavigationItem) {
        let item = UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .done, target: self, action: #selector(closeNavigationColumn))
        item.accessibilityLabel = "Close Column"
        if navigationItem.leftBarButtonItems != nil {
            navigationItem.leftBarButtonItems!.insert(item, at: 0)
        } else {
            navigationItem.leftBarButtonItems = [item]
        }
    }
    
    private func removeCloseBarButton(navigationItem: UINavigationItem) {
        navigationItem.leftBarButtonItems = (navigationItem.leftBarButtonItems ?? []).filter {
            $0.action != #selector(closeNavigationColumn)
        }
    }
    
    @objc private func closeNavigationColumn() {
        owner.closeColumn(contentViewController)
    }
}

private class ColumnNavigationController: UINavigationController {
    unowned let owner: MultiColumnNavigationController
    
    init(owner: MultiColumnNavigationController) {
        self.owner = owner
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func show(_ vc: UIViewController, sender: Any?) {
        owner.show(vc, sender: sender)
    }
}

private extension UIScrollView {
    var adjustedLeadingContentInset: CGFloat {
        if traitCollection.layoutDirection == .leftToRight {
            adjustedContentInset.left
        } else {
            adjustedContentInset.right
        }
    }
    
    var adjustedTrailingContentInset: CGFloat {
        if traitCollection.layoutDirection == .leftToRight {
            adjustedContentInset.right
        } else {
            adjustedContentInset.left
        }
    }
}
