//
//  RotorHandler.swift
//  MVMCoreUI
//
//  Created by Bandaru, Krishna Kishore on 13/10/23.
//  Copyright © 2023 Verizon Wireless. All rights reserved.
//

import Foundation
import Combine

//MARK: - RotorType that our app supports
fileprivate enum RotorType: String, CaseIterable {
    
    case button = "Buttons"
    case header = "Header"
    case link = "Link"
    
    var trait: UIAccessibilityTraits {
        switch self {
        case .button:
            return .button
        case .header:
            return .header
        case .link:
            return .link
        }
    }
    
    ///Filter block on model elements based on rotor type
    var modelFilter: ((MoleculeModelProtocol) -> Bool) {
        switch self {
        default:
            return { $0.accessibilityTraits?.contains(trait) ?? false }
        }
    }

    ///Filter block on model UIElements based on rotor type
    var filter: ((UIView) -> Bool) {
        switch self {
        default:
            return { $0.accessibilityTraits.contains(trait) && !$0.isHidden }
        }
    }
    
    ///Returns custom rotor of the specific type
    func getUIAccessibilityCustomRotor(itemSearch: @escaping UIAccessibilityCustomRotor.Search) -> UIAccessibilityCustomRotor? {
        var accessibilityCustomRotor: UIAccessibilityCustomRotor?
        switch self {
        case .header:
            accessibilityCustomRotor = UIAccessibilityCustomRotor(systemType: .heading, itemSearch: itemSearch)
        default:
            accessibilityCustomRotor = UIAccessibilityCustomRotor(name: rawValue, itemSearch: itemSearch)
        }
        return accessibilityCustomRotor
    }
}

//MARK: - RotorViewElementsProtocol
public protocol RotorViewElementsProtocol: MVMCoreViewControllerProtocol {
    var topView: UIView? { get set }
    var middleView: UIView? { get set }
    var bottomView: UIView? { get set }
}

//MARK: - RotorProtocols
public protocol RotorScrollDelegateProtocol: MVMCoreViewControllerProtocol {
    func scrollRectToVisible(_ rect: CGRect, animated: Bool)
}

public protocol RotorListTypeDelegateProtocol: RotorScrollDelegateProtocol {
    func scrollToRow(at indexPath: IndexPath, animated: Bool) -> Void
    func cellForRow(at indexPath: IndexPath) -> UIView?
    func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath?
}

//MARK: - Rotor Element - RotorElement info when we are traversing on model elements in the page.
struct RotorElement {
    
    let indexPath: IndexPath?
    let model: MoleculeModelProtocol
    let carouselItemModel: MoleculeModelProtocol?///This element is the parent of model item if we have rotor element inside carousel. This is used to scroll to specific carousel item when rotor mode is enabled.
    
    init(indexPath: IndexPath? = nil, model: MoleculeModelProtocol, carouselItemModel: MoleculeModelProtocol? = nil) {
        self.indexPath = indexPath
        self.model = model
        self.carouselItemModel = carouselItemModel
    }
}

//MARK: - Rotor Handler
class RotorHandler {
    
    private var rotorIndexes: [RotorType: Int] = [:]
    private var rotorElements: [RotorType: [Any]] = [:]
    private var anyCancellable: Set<AnyCancellable> = []
    private weak var delegateObject: MVMCoreUIDelegateObject?
    private weak var delegate: MVMCoreViewControllerProtocol? { delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol }
    private weak var accessibilityHandler: AccessibilityHandler?

    init(accessibilityHandler: AccessibilityHandler?) {
        self.accessibilityHandler = accessibilityHandler
        registerForVoiceOverChanges()
    }
    
    ///Preparing rotors when accessibility voiceover is turned after page is loaded.
    private func registerForVoiceOverChanges() {
        NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification)
            .sink { [weak self] _ in
                guard UIAccessibility.isVoiceOverRunning, (self?.rotorElements.isEmpty ?? true) else { return }
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                    self?.identifyAndPrepareRotors(self?.delegateObject)
                }
            }.store(in: &anyCancellable)
    }

    //MARK: - Pagevisibility behaviour methods.
    public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
        rotorIndexes = [:]
        rotorElements = [:]
        self.delegateObject = delegateObject
    }
    
    public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
        identifyAndPrepareRotors(delegateObject)
    }
    
    //MARK: - Rotor methods
    /**
     This method prepares custom rotors that will be assigned to the current controller.
     Rotor will be created only if the page contains that trait mapped element or the conditions met.
     */
    private func identifyAndPrepareRotors(_ delegateObject: MVMCoreUIDelegateObject?) {
        guard UIAccessibility.isVoiceOverRunning,
              let currentViewController = (delegateObject?.moleculeDelegate as? (MVMCoreViewControllerProtocol & UIViewController)) else { return }
        var customRotors: [UIAccessibilityCustomRotor] = []
        for type in RotorType.allCases {
            if let elements = getTraitMappedElements(currentViewController, type: type),
               !elements.isEmpty,
               let rotor = createRotor(for: type) {
                rotorElements[type] = elements
                customRotors.append(rotor)
            }
        }
        currentViewController.view.accessibilityCustomRotors = customRotors
    }
    
    /**
     This method prepares trait mapped elements of the current controller and from its manager if exists.
     */
    private func getTraitMappedElements(_ template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? {
        var accessibilityElements: [Any?] = []
        if let manager = (template as? MVMCoreViewManagerViewControllerProtocol)?.manager as? (MVMCoreViewControllerProtocol & UIViewController) {
            accessibilityElements.append(contentsOf: getRotorElementsFrom(manager: manager, type: type) ?? [])
        }
        if let rotorElements = getRotorElementsFrom(template: template, type: type) {
            accessibilityElements.append(contentsOf: rotorElements)
        }
        if let tabBarHidden = (template as? TabPageModelProtocol)?.tabBarHidden, !tabBarHidden {
            accessibilityElements.append(contentsOf: (MVMCoreUISplitViewController.main()?.tabBar?.subviews ?? []).filter { type.filter($0) })
        }
        return accessibilityElements.compactMap { $0 }
    }
    
    /**
     This method prepares trait mapped elements from manager
     */
    private func getRotorElementsFrom(manager: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? {
        guard let elements = (manager as? MVMCoreViewManagerProtocol)?.getAccessibilityElements() as? [UIView] else { return nil }
        return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: elements).filter { type.filter($0) } as [Any]
    }
    
    /**
     This method prepares triat mapped elements from the current viewcontroller.
     For BAU pages: This will traverse through the view hierarchy for trait mapped elements.
     For Molecular pages: This will traverse through the models to identify the trait mapped model. Along with traversed models, again we are traversing on view hierarchy if any subViews are added to view to get trait mapped elements.
     */
    private func getRotorElementsFrom(template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? {
        guard let template = template as? (RotorViewElementsProtocol & MVMCoreViewControllerProtocol & ViewController) else {
            return MVMCoreUIUtility.findViews(by: UIView.self, views: [template?.view].compactMap { $0 }).filter { type.filter($0) } as [Any] //BAU pages
        }
        let rotorElements = getRotorElements(from: template, type: type)
        let remainingRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.view], excludedViews: [template.topView, template.middleView, template.bottomView].compactMap { $0 }).filter { type.filter($0) } as [Any] //Other elements added to view if any.
        return rotorElements + remainingRotorElements
    }
    
    /**
     This method is to get rotor elements form Molecular pages only.
     We are filtering out header, molecules, footer from rootMolcules because rootMolcules may or maynot have the correct order of molecule models(header, body, footer). If we don't maintain order, Voiceover might first go footer then body elements then header.(Safety step)
     */
    private func getRotorElements(from template: (MVMCoreViewControllerProtocol & ViewController & RotorViewElementsProtocol)?, type: RotorType) -> [Any] {
        guard let templateModel = template?.model as? ThreeLayerModelBase else { return [] }
        var rotorElements: [Any] = []
        if let headerView = template?.topView {
            rotorElements.append(contentsOf: MVMCoreUIUtility.findViews(by: UIView.self, views: [headerView].compactMap { $0 }).filter { type.filter($0) })
        }
        let molecules = templateModel.rootMolecules.filter { !($0.id == templateModel.header?.id || $0.id == templateModel.footer?.id) }
        rotorElements.append(contentsOf: getRotorElements(from: molecules, template: template, type: type))
        if let bottomView = template?.bottomView {
            rotorElements.append(contentsOf: MVMCoreUIUtility.findViews(by: UIView.self, views: [bottomView].compactMap { $0 }).filter { type.filter($0) })
        }
        return rotorElements
    }
    
    /**
     This method actually travers through the molecules and identify triat mapped model element along with indexPath of the element if its List/Collection templates.
     Along with model, indexPath we are also capturing carouselItemModel because when we have Carousel inside the molecule, we need to scroll to carousel item if we have trait mapped rotor element inside the Carousel - (Multi Scroll behaviour)
     Identiying the CarouselModel, CarouselItemModel by depth.
     */
    private func getRotorElements(from molecules: [MoleculeModelProtocol], template: (MVMCoreViewControllerProtocol & ViewController)?, type: RotorType) -> [Any] {
        var traitIndexPath: IndexPath?
        var carouselItemModel: MoleculeModelProtocol?
        var carouselDepth: Int?
        let rotorElements: [RotorElement] = molecules.reduceDepthFirstTraverse(options: .parentFirst, depth: 0, initialResult: []) { result, model, depth in
            if let listModel = model as? (ListItemModelProtocol & MoleculeModelProtocol),
               let indexPath = (template as? RotorListTypeDelegateProtocol)?.getIndexPathFor(molecule: listModel) {
                traitIndexPath = indexPath
            }
            if let _carouselDepth = carouselDepth, depth < _carouselDepth {
                carouselDepth = nil
                carouselItemModel = nil
            }
            if model is CarouselModel { carouselDepth = depth }
            carouselItemModel = model is CarouselItemModelProtocol ? model : carouselItemModel
            var result = result
            if type.modelFilter(model) {
                result.append(.init(indexPath: traitIndexPath, model: model, carouselItemModel: carouselItemModel))
            }
            return result
        }
        return rotorElements as [Any]
    }
    
    /**
     This method creates a rotor based on the RotorType.
     UIAccessibilityCustomRotor.Search Predicate block is used to return the current rotor UI element
     If the rotor element is of type UIElement(subclasses of UIView) then it will post the accessibility notification directly to that UI element and scrolling that element to the visible area.
     If rotor element is of type RotorElement then
            1. If we have indexPath in rotorElement then the template will scroll (list/collection) to that indexPath, and try to capture the cell view.
            2. After the cell view is captured, traversing the cell hierarchy which matches the trait & id of that view's model.
            3. After identifying the element, then will post the accessibility notification directly to that UI element
        If we have carouselItemModel in RotorElement then
            1. If we have indexPath in rotorElement then the template will scroll (list/collection) to that indexPath, and try to capture the cell view.
            2. After cell is identified then we are identifying Carousel from the view hierarchy & scroll to the Carousel item which matches the carouselItemModel.id
            3. After carouselItemModel is scrolled then traversing the carouselCellItem hierarchy which matches the trait & id of that view's model
            4. After identifying the element, then will post the accessibility notification directly to that UI element
     */
    private func createRotor(for type: RotorType) -> UIAccessibilityCustomRotor? {
        return type.getUIAccessibilityCustomRotor { [weak self] predicate in
            guard let self, let elements = self.rotorElements[type] else { return UIAccessibilityCustomRotorItemResult() }
            var rotorIndex = self.rotorIndexes[type] ?? -1
            switch predicate.searchDirection {
            case .next:
                if rotorIndex + 1 < elements.count {
                    rotorIndex += 1
                }
            case .previous:
                if rotorIndex > 0 {
                    rotorIndex -= 1
                }
            @unknown default:
                rotorIndex = 0
            }
            guard rotorIndex >= 0, !elements.isEmpty else { return UIAccessibilityCustomRotorItemResult() } //Safety check to avoid crash.
            var rotorElement = elements[rotorIndex]
            if let element = rotorElement as? RotorElement,
               let controller = self.delegate as? (RotorViewElementsProtocol & ViewController) {
                var cellView: UIView?
                if let indexPath = element.indexPath, let controller = self.delegate as? (any RotorListTypeDelegateProtocol) {
                    controller.scrollToRow(at: indexPath, animated: false)
                    cellView = controller.cellForRow(at: indexPath)
                } else {
                    cellView = controller.view
                }
                if let cauroselItemModel = element.carouselItemModel {
                    guard let carousel = MVMCoreUIUtility.findViews(by: Carousel.self, views: [cellView].compactMap { $0 }).first,
                          let index = (carousel.molecules?.firstIndex { $0.id == cauroselItemModel.id }) else { return UIAccessibilityCustomRotorItemResult() }
                    let collectionIndexPath = IndexPath(item: index, section: 0)
                    carousel.collectionView.scrollToItem(at: collectionIndexPath, at: .left, animated: false)
                    let collectionViewCell = carousel.collectionView.cellForItem(at: collectionIndexPath)
                    rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [collectionViewCell].compactMap { $0 }).filter { type.filter($0) && $0.model?.id == element.model.id }.first as Any
                } else {
                    rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [cellView].compactMap { $0 }).filter { type.filter($0) && $0.model?.id == element.model.id }.first as Any
                    if let viewElement = (rotorElement as? UIView) {
                        let convertedFrame = viewElement.convert(viewElement.frame, to: controller.view)
                        (self.delegate as? RotorScrollDelegateProtocol)?.scrollRectToVisible(convertedFrame, animated: false)
                    }
                }
            } else {
                if let viewElement = (rotorElement as? UIView) {
                    let convertedFrame = viewElement.convert(viewElement.frame, to: (self.delegate as? UIViewController)?.view)
                    (self.delegate as? RotorScrollDelegateProtocol)?.scrollRectToVisible(convertedFrame, animated: false)
                }
            }
            self.rotorIndexes[type] = rotorIndex
            self.accessibilityHandler?.post(notification: .layoutChanged, argument: rotorElement)
            return UIAccessibilityCustomRotorItemResult(targetElement: rotorElement as! NSObjectProtocol, targetRange: nil)
        }
    }
}

//MARK: - Protocol Extensions
public extension RotorViewElementsProtocol {
    var topView: UIView? { nil }
    var middleView: UIView? { nil }
    var bottomView: UIView? { nil }
}

extension RotorScrollDelegateProtocol {
    public func scrollRectToVisible(_ rect: CGRect, animated: Bool) { }
}

extension RotorListTypeDelegateProtocol {
    
    public func scrollToRow(at indexPath: IndexPath, animated: Bool) { }
    public func cellForRow(at indexPath: IndexPath) -> UIView? { nil }
    public func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? { nil }
}

extension ThreeLayerTableViewController: RotorListTypeDelegateProtocol {
    
    public func scrollToRow(at indexPath: IndexPath, animated: Bool) {
        tableView.scrollToRow(at: indexPath, at: .middle, animated: animated)
    }
    
    public func cellForRow(at indexPath: IndexPath) -> UIView? {
        tableView.cellForRow(at: indexPath)
    }
    
    public func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? {
        guard let molecule = molecule as? (ListItemModelProtocol & MoleculeModelProtocol) else { return nil }
        return (self as? MoleculeListTemplate)?.getIndexPath(for: molecule)
    }
}

extension ThreeLayerViewController: RotorScrollDelegateProtocol {
    
    public func scrollRectToVisible(_ rect: CGRect, animated: Bool) {
        scrollView?.scrollRectToVisible(rect, animated: animated)
    }
}

extension ThreeLayerCollectionViewController: RotorListTypeDelegateProtocol {
    
    public func scrollToRow(at indexPath: IndexPath, animated: Bool) {
        collectionView?.scrollToItem(at: indexPath, at: .centeredVertically, animated: animated)
    }
    
    public func cellForRow(at indexPath: IndexPath) -> UIView? {
        collectionView?.cellForItem(at: indexPath)
    }
    
    public func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? {
        guard let molecule = molecule as? (CollectionItemModelProtocol & MoleculeModelProtocol) else { return nil }
        return (self as? MoleculeCollectionListProtocol)?.getIndexPath(for: molecule)
    }
}
