//
//  FastAccountSwitcherViewController.swift
//  Tusker
//
//  Created by Shadowfacts on 11/4/20.
//  Copyright © 2020 Shadowfacts. All rights reserved.
//

import UIKit
import UserAccounts

protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
    func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController)
    /// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached.
    func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
}

class FastAccountSwitcherViewController: UIViewController {
    
    weak var delegate: FastAccountSwitcherViewControllerDelegate?
    
    @IBOutlet weak var dimmingView: UIView!
    @IBOutlet weak var blurContentView: UIView!
    @IBOutlet weak var accountsStack: UIStackView!
    
    private(set) var accountViews: [FastSwitchingAccountView] = []
    private var lastSelectedAccountViewIndex: Int?
    private var selectionChangedFeedbackGenerator: UISelectionFeedbackGenerator?
    private var touchBeganFeedbackWorkItem: DispatchWorkItem?
    
    var itemOrientation: ItemOrientation = .iconsTrailing
    
    init() {
        super.init(nibName: "FastAccountSwitcherViewController", bundle: .main)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        view.isHidden = true
        
        view.accessibilityViewIsModal = true
        
        view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))))
        accountsStack.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))))
    }
    
    func createSwitcherGesture() -> UIGestureRecognizer {
        let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
        recognizer.delegate = self
        recognizer.minimumPressDuration = 0.25
        return recognizer
    }
    
    func show() {
        createAccountViews()
        // add after creating account views so that the presenter can align based on them
        delegate?.fastAccountSwitcherAddToViewHierarchy(self)
        
        switch itemOrientation {
        case .iconsLeading:
            accountsStack.alignment = .leading
        case .iconsTrailing:
            accountsStack.alignment = .trailing
        }
        
        view.isHidden = false
        
        func completion() {
            UIAccessibility.post(notification: .screenChanged, argument: accountViews.first)
        }
        
        if UIAccessibility.prefersCrossFadeTransitions {
            view.alpha = 0
            UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
                self.view.alpha = 1
            } completion: { _ in
                completion()
            }
        } else {
            let totalDuration: TimeInterval = 0.5
            UIView.animateKeyframes(withDuration: totalDuration, delay: 0, options: .allowUserInteraction) {
                self.view.alpha = 0
                UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
                    self.view.alpha = 1
                }
                
                for (index, accountView) in self.accountViews.reversed().enumerated() {
                    let relStart = 0.5 * Double(index) / Double(self.accountsStack.arrangedSubviews.count)
                    let relDuration = 0.5 * 2 / Double(self.accountsStack.arrangedSubviews.count)
                    
                    accountView.alpha = 0
                    accountView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
                    UIView.addKeyframe(withRelativeStartTime: relStart, relativeDuration: relDuration / 2) {
                        accountView.alpha = 1
                        accountView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
                    }
                    
                    UIView.addKeyframe(withRelativeStartTime: relStart + relDuration / 2, relativeDuration: relDuration / 2) {
                        accountView.transform = .identity
                    }
                }
            } completion: { _ in
                completion()
            }
        }
    }
    
    func hide(completion: (() -> Void)? = nil) {
        guard view.superview != nil else {
            return
        }
        lastSelectedAccountViewIndex = nil
        selectionChangedFeedbackGenerator = nil
        
        UIView.animate(withDuration: 0.15, delay: 0, options: .curveEaseInOut) {
            self.view.alpha = 0
        } completion: { (_) in
            // todo: probably remove these two lines
            self.view.alpha = 1
            self.view.isHidden = true
            completion?()
            self.view.removeFromSuperview()
            
            UIAccessibility.post(notification: .screenChanged, argument: nil)
        }
    }
    
    private func createAccountViews() {
        accountsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
        
        let addAccountPlaceholder = FastSwitchingAccountView(orientation: itemOrientation)
        accountsStack.addArrangedSubview(addAccountPlaceholder)
        
        accountViews = [
            addAccountPlaceholder
        ]
        
        for account in UserAccountsManager.shared.accounts {
            let accountView = FastSwitchingAccountView(account: account, orientation: itemOrientation)
            accountView.isCurrent = account.id == UserAccountsManager.shared.mostRecentAccountID
            accountsStack.addArrangedSubview(accountView)
            accountViews.append(accountView)
        }
    }
    
    private func accountViewIndex(at point: CGPoint) -> Int? {
        for (index, accountView) in accountViews.enumerated() {
            let pointInAccountView = accountView.convert(point, from: view)
            if accountView.bounds.contains(pointInAccountView) {
                return index
            }
        }
        return nil
    }
    
    private func switchAccount(newIndex: Int, hapticFeedback: Bool = true) {
        if newIndex == 0 { // add account placeholder
            if hapticFeedback {
                selectionChangedFeedbackGenerator?.selectionChanged()
            }
            selectionChangedFeedbackGenerator = nil
            
            hide() {
                (self.view.window!.windowScene!.delegate as! MainSceneDelegate).showAddAccount()
            }
        } else {
            let account = UserAccountsManager.shared.accounts[newIndex - 1]
            
            if account.id != UserAccountsManager.shared.mostRecentAccountID {
                if hapticFeedback {
                    selectionChangedFeedbackGenerator?.selectionChanged()
                }
                selectionChangedFeedbackGenerator = nil
                
                hide() {
                    (self.view.window!.windowScene!.delegate as! MainSceneDelegate).activateAccount(account, animated: true)
                }
            } else {
                hide()
            }
        }
    }
    
    // MARK: - Interaction
    
    @objc private func handleLongPress(_ recognizer: UIGestureRecognizer) {
        switch recognizer.state {
        case .began:
            UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
            selectionChangedFeedbackGenerator = UISelectionFeedbackGenerator()
            selectionChangedFeedbackGenerator?.prepare()
            
            show()
            
        case .changed:
            let location = recognizer.location(in: view)
            
            handleGestureMoved(to: location)
            
        case .ended:
            if let index = lastSelectedAccountViewIndex {
                switchAccount(newIndex: index)
            } else if !(delegate?.fastAccountSwitcher(self, triggerZoneContains: recognizer.location(in: recognizer.view)) ?? false) {
                hide()
            }
            
        default:
            break
        }
    }
    
    @objc private func handlePan(_ recognizer: UIPanGestureRecognizer) {
        switch recognizer.state {
        case .changed:
            let location = recognizer.location(in: view)
            
            handleGestureMoved(to: location)
            
        case .ended:
            if let index = lastSelectedAccountViewIndex {
                switchAccount(newIndex: index)
            }
            
        default:
            break
        }
    }
    
    private func handleGestureMoved(to location: CGPoint, hapticFeedback: Bool = true) {
        let selectedAccountViewIndex = accountViewIndex(at: location)
        
        if selectedAccountViewIndex != lastSelectedAccountViewIndex {
            if let lastSelected = lastSelectedAccountViewIndex {
                accountViews[lastSelected].isSelected = false
            }
            if let newSelected = selectedAccountViewIndex {
                accountViews[newSelected].isSelected = true
            }
            lastSelectedAccountViewIndex = selectedAccountViewIndex
            
            if hapticFeedback {
                selectionChangedFeedbackGenerator?.selectionChanged()
                selectionChangedFeedbackGenerator?.prepare()
            }
        }
    }
    
    @objc private func handleTap(_ recognizer: UITapGestureRecognizer) {
        if let tappedIndex = accountViewIndex(at: recognizer.location(in: view)) {
            // cancel the selection-changed feedback initiated in touchesBegan so that,
            // upon switching, we don't trigger a double feedback
            touchBeganFeedbackWorkItem?.cancel()
            touchBeganFeedbackWorkItem = nil
            
            switchAccount(newIndex: tappedIndex)
        } else {
            hide()
        }
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if touches.count == 1,
           let touch = touches.first,
           accountsStack.bounds.contains(touch.location(in: accountsStack)) {
            handleGestureMoved(to: touch.location(in: view), hapticFeedback: false)
            
            // don't trigger the haptic feedback immedaitely
            // if the user is merely tapping, not initiating a pan, we don't want to trigger a double-impact
            // if the tap ends very quickly, this will be cancelled
            touchBeganFeedbackWorkItem = DispatchWorkItem {
                self.selectionChangedFeedbackGenerator?.selectionChanged()
                self.selectionChangedFeedbackGenerator?.prepare()
                self.touchBeganFeedbackWorkItem = nil
            }
            // 100ms determined experimentally to be fast enough that there's not a hugely-perceivable delay when beginning a pan gesture
            // and slow enough that it's longer than most reasonable-speed taps
            DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100), execute: touchBeganFeedbackWorkItem!)
        }
        
        super.touchesBegan(touches, with: event)
    }
    
    override func accessibilityPerformEscape() -> Bool {
        guard !view.isHidden else {
            return false
        }
        hide()
        return true
    }

}

extension FastAccountSwitcherViewController {
    enum ItemOrientation {
        case iconsLeading
        case iconsTrailing
    }
}

extension FastAccountSwitcherViewController: UIGestureRecognizerDelegate {
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        let point = gestureRecognizer.location(in: gestureRecognizer.view)
        return delegate?.fastAccountSwitcher(self, triggerZoneContains: point) ?? false
    }
}
