//
//  CustomAlertController.swift
//  Tusker
//
//  Created by Shadowfacts on 9/17/22.
//  Copyright © 2022 Shadowfacts. All rights reserved.
//

import UIKit

class CustomAlertController: UIViewController {
    private let config: Configuration
    
    fileprivate var blurView: UIVisualEffectView!
    fileprivate var dimmingView: UIView!
    fileprivate var buttonsStack: UIStackView!
    fileprivate var actionsView: CustomAlertActionsView!
    
    init(config: Configuration) {
        self.config = config
        
        super.init(nibName: nil, bundle: nil)
        
        transitioningDelegate = self
        modalPresentationStyle = .custom
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .clear
        
        dimmingView = UIView()
        dimmingView.backgroundColor = .black.withAlphaComponent(0.2)
        dimmingView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(dimmingView)
        NSLayoutConstraint.activate([
            dimmingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            dimmingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            dimmingView.topAnchor.constraint(equalTo: view.topAnchor),
            dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
        
        blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
        
        blurView.backgroundColor = .appBackground
        blurView.layer.cornerRadius = 15
        blurView.layer.cornerCurve = .continuous
        blurView.layer.masksToBounds = true
        blurView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(blurView)
        NSLayoutConstraint.activate([
            blurView.widthAnchor.constraint(equalToConstant: 270),
            blurView.heightAnchor.constraint(greaterThanOrEqualToConstant: 100),
            blurView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            blurView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])

        let stack = UIStackView()
        stack.axis = .vertical
        stack.spacing = 0
        stack.alignment = .fill
        stack.translatesAutoresizingMaskIntoConstraints = false
        blurView.contentView.addSubview(stack)
        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
            blurView.contentView.trailingAnchor.constraint(equalTo: stack.trailingAnchor),
            stack.topAnchor.constraint(equalTo: blurView.contentView.topAnchor, constant: 16),
            stack.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor),
        ])
        
        let titleLabel = UILabel()
        titleLabel.text = config.title
        titleLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold)!, size: 0)
        titleLabel.adjustsFontForContentSizeCategory = true
        titleLabel.numberOfLines = 0
        titleLabel.textAlignment = .center
        stack.addArrangedSubview(titleLabel)
        
        stack.addSpacer(length: 8)
        
        stack.addArrangedSubview(config.content)
        
        stack.addSpacer(length: 16)
        
        let separator = UIView()
        separator.tag = ViewTags.customAlertSeparator
        separator.backgroundColor = .separator
        stack.addArrangedSubview(separator)
        NSLayoutConstraint.activate([
            separator.widthAnchor.constraint(equalTo: stack.widthAnchor),
            separator.heightAnchor.constraint(equalToConstant: 0.5),
        ])
        
        actionsView = CustomAlertActionsView(config: config, dismiss: { [unowned self] in
            self.dismiss(animated: true)
        })
        stack.addArrangedSubview(actionsView)
        NSLayoutConstraint.activate([
            actionsView.widthAnchor.constraint(equalTo: stack.widthAnchor),
        ])
    }
    
    struct Configuration {
        var title: String
        var content: UIView
        var actions: [Action]
    }
    
    struct Action {
        var title: String?
        var image: UIImage?
        var style: Style
        var handler: (() -> Void)?
        var isSecondaryMenu: Bool = false
        
        init(title: String?, image: UIImage? = nil, style: Style, handler: (() -> Void)?) {
            self.title = title
            self.image = image
            self.style = style
            self.handler = handler
        }
        
        enum Style {
            case `default`, cancel, destructive, menu([MenuAction])
        }
    }
    
    struct MenuAction {
        var title: String
        var subtitle: String?
        var image: UIImage?
        var handler: () -> Void
    }
}

class CustomAlertActionsView: UIControl {
    private let dismiss: () -> Void
    
    private let stack = UIStackView()
    private var actionButtons: [CustomAlertActionButton] = []
    private var labelWrapperWidthConstraints: [NSLayoutConstraint] = []
    // the actions from the config but reordered to match labelWrappers order
    private var reorderedActions: [CustomAlertController.Action] = []
    private var separators: [UIView] = []
    private var separatorSizeConstraints: [NSLayoutConstraint] = []
    
    private let generator = UISelectionFeedbackGenerator()
    private var currentSelectedActionIndex: Int?
    
    init(config: CustomAlertController.Configuration, dismiss: @escaping () -> Void) {
        self.dismiss = dismiss
        
        super.init(frame: .zero)
        
        stack.alignment = .fill
        stack.translatesAutoresizingMaskIntoConstraints = false
        addSubview(stack)
        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: leadingAnchor),
            stack.trailingAnchor.constraint(equalTo: trailingAnchor),
            stack.topAnchor.constraint(equalTo: topAnchor),
            stack.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])
        
        for action in config.actions {
            let button = CustomAlertActionButton(action: action, dismiss: dismiss)
            
            button.isAccessibilityElement = true
            button.accessibilityTraits = .button
            button.accessibilityRespondsToUserInteraction = true
            button.accessibilityLabel = action.title
            
            if action.isSecondaryMenu {
                button.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true
            }
            
            if case .cancel = action.style {
                actionButtons.insert(button, at: 0)
                reorderedActions.insert(action, at: 0)
            } else {
                actionButtons.append(button)
                reorderedActions.append(action)
            }
        }
        
        var first = true
        for (action, button) in zip(reorderedActions, actionButtons) {
            if first {
                first = false
            } else if !action.isSecondaryMenu {
                let separator = UIView()
                separator.tag = ViewTags.customAlertSeparator
                separator.backgroundColor = .separator
                stack.addArrangedSubview(separator)
                separators.append(separator)
            }
            if action.isSecondaryMenu {
                // prev button
                let prev = stack.arrangedSubviews.last!
                stack.removeArrangedSubview(prev)
                let separator = UIView()
                separator.tag = ViewTags.customAlertSeparator
                separator.backgroundColor = .separator
                separator.widthAnchor.constraint(equalToConstant: 0.5).isActive = true
                let hStack = UIStackView(arrangedSubviews: [prev, separator, button])
                hStack.axis = .horizontal
                stack.addArrangedSubview(hStack)
            } else {
                stack.addArrangedSubview(button)
            }
        }
        
        addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panRecognized)))
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        updateAxis()
        super.layoutSubviews()
    }
    
    private var menuActionsCount: Int {
        var count = 0
        for action in reorderedActions {
            if case .menu(_) = action.style {
                count += 1
            }
        }
        return count
    }
    
    private var needsVertical: Bool {
        if reorderedActions.count - menuActionsCount > 2 {
            return false
        }
        var requiredWidth: CGFloat = 0
        for (index, action) in actionButtons.enumerated() {
            if index > 0 {
                requiredWidth += 0.5
            }
            requiredWidth += action.titleView.systemLayoutSizeFitting(UIView.layoutFittingExpandedSize).width
            requiredWidth += 8
        }
        return requiredWidth > bounds.width
    }
    
    private func updateAxis() {
        if needsVertical {
            stack.axis = .vertical
            NSLayoutConstraint.deactivate(labelWrapperWidthConstraints)
            labelWrapperWidthConstraints = []
            NSLayoutConstraint.deactivate(separatorSizeConstraints)
            separatorSizeConstraints = separators.map {
                $0.heightAnchor.constraint(equalToConstant: 0.5)
            }
            NSLayoutConstraint.activate(separatorSizeConstraints)
        } else {
            stack.axis = .horizontal
            labelWrapperWidthConstraints = []
            var nonMenuAction: CustomAlertActionButton?
            for (index, action) in reorderedActions.enumerated() {
                if case .menu(_) = action.style {
                } else {
                    if let nonMenuAction {
                        labelWrapperWidthConstraints.append(
                            actionButtons[index].widthAnchor.constraint(equalTo: nonMenuAction.widthAnchor)
                        )
                    } else {
                        nonMenuAction = actionButtons[index]
                    }
                }
            }
            NSLayoutConstraint.activate(labelWrapperWidthConstraints)
            NSLayoutConstraint.deactivate(separatorSizeConstraints)
            separatorSizeConstraints = separators.map {
                $0.widthAnchor.constraint(equalToConstant: 0.5)
            }
            NSLayoutConstraint.activate(separatorSizeConstraints)
        }
    }
    
    @objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
        let selectedButton = actionButtons.enumerated().first {
            $0.1.point(inside: recognizer.location(in: $0.1), with: nil)
        }
        
        switch recognizer.state {
        case .began:
            currentSelectedActionIndex = selectedButton?.offset
            selectedButton?.element.backgroundColor = .secondarySystemFill
            
            generator.prepare()
            
        case .changed:
            if selectedButton == nil && hitTest(recognizer.location(in: self), with: nil)?.tag == ViewTags.customAlertSeparator {
                break
            }
            
            if selectedButton?.offset != currentSelectedActionIndex {
                if let currentSelectedActionIndex {
                    actionButtons[currentSelectedActionIndex].backgroundColor = nil
                }
                generator.selectionChanged()
            }
            
            currentSelectedActionIndex = selectedButton?.offset
            selectedButton?.element.backgroundColor = .secondarySystemFill
            
            generator.prepare()
            
        case .ended:
            if let currentSelectedActionIndex {
                let button = actionButtons[currentSelectedActionIndex]
                button.backgroundColor = nil
                let action = reorderedActions[currentSelectedActionIndex]
                if action.handler == nil,
                   case .menu(_) = action.style,
                   let interaction = button.contextMenuInteraction {
                    let selector = NSSelectorFromString(["Location:", "At", "Menu", "present", "_"].reversed().joined())
                    if interaction.responds(to: selector) {
                        interaction.perform(selector, with: recognizer.location(in: button))
                    }
                } else {
                    action.handler?()
                    self.dismiss()
                }
            }
            
        default:
            break
        }
    }
}

class CustomAlertActionButton: UIControl {
    private let action: CustomAlertController.Action
    private let dismiss: () -> Void
    
    var titleView = UIStackView()
    
    init(action: CustomAlertController.Action, dismiss: @escaping () -> Void) {
        precondition(action.title != nil || action.image != nil, "action must have image and/or title")
        
        self.action = action
        self.dismiss = dismiss
        
        super.init(frame: .zero)
        
        titleView = UIStackView()
        titleView.axis = .horizontal
        titleView.spacing = 4
        titleView.alignment = .center
        
        if let title = action.title {
            let label = UILabel()
            label.text = title
            label.textColor = .tintColor
            label.adjustsFontForContentSizeCategory = true
            if case .cancel = action.style {
                label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold)!, size: 0)
            } else {
                label.font = .preferredFont(forTextStyle: .body)
            }
            if case .destructive = action.style {
                label.textColor = .systemRed
            }
            titleView.addArrangedSubview(label)
        }
        
        if let image = action.image {
            let imageView = UIImageView(image: image)
            titleView.addArrangedSubview(imageView)
        }
        
        titleView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(titleView)
        NSLayoutConstraint.activate([
            titleView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 4),
            titleView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -4),
            titleView.centerXAnchor.constraint(equalTo: centerXAnchor),
            titleView.centerYAnchor.constraint(equalTo: centerYAnchor),
            heightAnchor.constraint(equalToConstant: 44),
        ])
        
        if case .menu(_) = action.style {
            self.isContextMenuInteractionEnabled = true
            self.showsMenuAsPrimaryAction = action.handler == nil
        }
        
        addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(hoverRecognized)))
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
        guard case .menu(let menuActions) = action.style else {
            return nil
        }
        return UIContextMenuConfiguration(actionProvider: { _ in
            return UIMenu(children: menuActions.map { action in
                UIAction(title: action.title, subtitle: action.subtitle, image: action.image) { [unowned self] _ in
                    action.handler()
                    self.dismiss()
                }
            })
        })
    }
    
    override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
        super.contextMenuInteraction(interaction, willDisplayMenuFor: configuration, animator: animator)
        if let animator {
            animator.addAnimations {
                self.backgroundColor = nil
            }
        } else {
            backgroundColor = nil
        }
    }
    
    override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
        super.contextMenuInteraction(interaction, willEndFor: configuration, animator: animator)
    }
    
    @objc func hoverRecognized(_ recognizer: UIHoverGestureRecognizer) {
        switch recognizer.state {
        case .began, .changed:
            backgroundColor = .secondarySystemFill
        case .ended:
            backgroundColor = nil
        default:
            break
        }
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        
        if point(inside: touches.first!.location(in: self), with: event) {
            backgroundColor = .secondarySystemFill
        }
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesMoved(touches, with: event)
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        backgroundColor = nil
        action.handler?()
        dismiss()
    }
}

extension CustomAlertController {
    override func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CustomAlertPresentationAnimation()
    }
    
    override func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CustomAlertDismissAnimation()
    }
}

class CustomAlertPresentationAnimation: NSObject, UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.2
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let presenter = transitionContext.viewController(forKey: .from),
              let alert = transitionContext.viewController(forKey: .to) as? CustomAlertController else {
            transitionContext.completeTransition(false)
            return
        }
        
        let container = transitionContext.containerView
        container.addSubview(alert.view)
        alert.view.frame = container.bounds
        
        guard transitionContext.isAnimated else {
            presenter.view.tintAdjustmentMode = .dimmed
            transitionContext.completeTransition(true)
            return
        }
        
        alert.dimmingView.layer.opacity = 0
        alert.blurView.layer.opacity = 0
        alert.blurView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
        
        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveEaseOut) {
            presenter.view.tintAdjustmentMode = .dimmed
            alert.dimmingView.layer.opacity = 1
            alert.blurView.layer.opacity = 1
            alert.blurView.transform = .identity
        } completion: { _ in
            transitionContext.completeTransition(true)
        }
    }
}

class CustomAlertDismissAnimation: NSObject, UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.2
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let presenter = transitionContext.viewController(forKey: .to),
              let alert = transitionContext.viewController(forKey: .from) as? CustomAlertController else {
            transitionContext.completeTransition(false)
            return
        }
        
        guard transitionContext.isAnimated else {
            presenter.view.tintAdjustmentMode = .dimmed
            transitionContext.completeTransition(true)
            return
        }
        
        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0) {
            presenter.view.tintAdjustmentMode = .automatic
            alert.view.layer.opacity = 0
        } completion: { _ in
            transitionContext.completeTransition(true)
        }
    }
}

fileprivate extension UIStackView {
    func addSpacer(length: CGFloat) {
        let spacer = UIView()
        addArrangedSubview(spacer)
        if axis == .vertical {
            spacer.heightAnchor.constraint(equalToConstant: length).isActive = true
        } else {
            spacer.widthAnchor.constraint(equalToConstant: length).isActive = true
        }
    }
}

