//
//  MoleculeListTemplate.swift
//  MVMCoreUI
//
//  Created by Scott Pfeil on 4/18/19.
//  Copyright © 2019 Verizon Wireless. All rights reserved.
//

import UIKit

public typealias MoleculeInfo = (identifier: String, class: AnyClass, molecule: MoleculeModelProtocol)


open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol {
    //--------------------------------------------------
    // MARK: - Stored Properties
    //--------------------------------------------------
    
    public var moleculesInfo: [MoleculeInfo]?
    
    var observer: NSKeyValueObservation?
    
    public var templateModel: ListPageTemplateModel?
    
    //--------------------------------------------------
    // MARK: - Computed Properties
    //--------------------------------------------------
    
    open override var loadObject: MVMCoreLoadObject? {
        didSet {
            guard loadObject != oldValue else { return }
            
            updateRequiredModules()
            observer?.invalidate()
            if let newObject = loadObject {
                observer = newObject.observe(\MVMCoreLoadObject.pageJSON, options: [.old, .new]) { [weak self] object, change in
                    self?.updateRequiredModules()
                }
            }
        }
    }
    
    open var hasSingleCellSelection: Bool {
        templateModel?.singleCellSelection ?? false
    }
    
    //--------------------------------------------------
    // MARK: - Methods
    //--------------------------------------------------
    
    open override func parsePageJSON() throws {
        try parseTemplate(json: loadObject?.pageJSON)
        try super.parsePageJSON()
    }
    
    // For subclassing the model.
    open func decodeTemplate(using decoder: JSONDecoder, from data: Data) throws -> ListPageTemplateModel {
        try decoder.decode(ListPageTemplateModel.self, from: data)
    }
    
    open override func viewForTop() -> UIView {
        guard let headerModel = templateModel?.header,
              let molecule = ModelRegistry.createMolecule(headerModel, delegateObject: delegateObjectIVar)
        else { return super.viewForTop() }
        
        // Temporary, Default the horizontal padding
        if var container = templateModel?.header as? ContainerModelProtocol, container.useHorizontalMargins == nil {
            container.useHorizontalMargins = true
        }
        
        return molecule
    }
    
    override open func viewForBottom() -> UIView {
        guard let footerModel = templateModel?.footer,
              let molecule = ModelRegistry.createMolecule(footerModel, delegateObject: delegateObjectIVar)
        else { return super.viewForBottom() }
        
        return molecule
    }
    
    open override func handleNewData() {
        topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false
        bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false
        setup()
        registerWithTable()
        super.handleNewData()
    }
    
    open override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        handleScrollToSpecificRow()
    }
    
    //--------------------------------------------------
    // MARK: - Handle scroll to spefic row
    //--------------------------------------------------
    
    func handleScrollToSpecificRow() {
        guard let index = templateModel?.scrollToRowIndex,
              loadObject?.pageDataFromCache == false,
              let table = tableView,
              index < tableView(table, numberOfRowsInSection:0)
        else { return }
        
        let indexPath = IndexPath(row: index, section: 0)
        tableView?.scrollToRow(at: indexPath, at: .middle, animated: false)
        //Reset for without refresh page not needed
        templateModel?.scrollToRowIndex = nil
    }
    
    //--------------------------------------------------
    // MARK: - Table View
    //--------------------------------------------------
    
    open override func registerWithTable() {
        super.registerWithTable()
        guard let moleculesInfo = moleculesInfo else { return }
        
        for moleculeInfo in moleculesInfo {
            tableView?.register(moleculeInfo.class, forCellReuseIdentifier: moleculeInfo.identifier)
        }
    }
    
    open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        guard let moleculeInfo = getMoleculeInfo(for: indexPath),
              let estimatedHeight = (moleculeInfo.class as? MoleculeViewProtocol.Type)?.estimatedHeight(with: moleculeInfo.molecule, delegateObject() as? MVMCoreUIDelegateObject)
        else { return 0 }
        
        return estimatedHeight
    }
    
    open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        moleculesInfo?.count ?? 0
    }
    
    open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let moleculeInfo = getMoleculeInfo(for: indexPath),
              let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.identifier)
        else { return UITableViewCell() }
        
        (cell as? MoleculeViewProtocol)?.reset()
        (cell as? MoleculeListCellProtocol)?.setLines(with: templateModel?.line, delegateObject: delegateObjectIVar, additionalData: nil, indexPath: indexPath)
        (cell as? MoleculeViewProtocol)?.set(with: moleculeInfo.molecule, delegateObjectIVar, nil)
        (cell as? MVMCoreViewProtocol)?.updateView(tableView.bounds.width)
        // Neded to fix an apple defect where the cell is not the correct size on certain devices for certain cells
        cell.layoutIfNeeded()
        return cell
    }
    
    open func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        (cell as? MoleculeListCellProtocol)?.willDisplay()
    }
    
    open func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
        guard let cell = tableView.cellForRow(at: indexPath) as? MoleculeListCellProtocol,
              !cell.willSelectCell(at: indexPath, delegateObject: delegateObject() as? MVMCoreUIDelegateObject, additionalData: nil)
        else { return indexPath }

        return nil
    }

    open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let cell = tableView.cellForRow(at: indexPath) as? MoleculeListCellProtocol else { return }
        
        cell.didSelectCell(at: indexPath, delegateObject: delegateObject() as? MVMCoreUIDelegateObject, additionalData: nil)
    }
    
    open override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        guard let cell = tableView.cellForRow(at: indexPath) as? MoleculeListCellProtocol else { return }
        
        cell.didDeselectCell(at: indexPath, delegateObject: delegateObject() as? MVMCoreUIDelegateObject, additionalData: nil)
    }
    
    //--------------------------------------------------
    // MARK: - MoleculeDelegateProtocol
    //--------------------------------------------------
    
    open override func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) {
        guard let tableView = tableView else { return }

        let point = molecule.convert(molecule.bounds.origin, to: tableView)
        if let indexPath = tableView.indexPathForRow(at: point), tableView.indexPathsForVisibleRows?.contains(indexPath) ?? false {
            refreshTable()
        }
    }
    
    open func newData(for molecule: MoleculeModelProtocol) {
        //TODO: expand for header, navigation, etc
        guard let index = moleculesInfo?.firstIndex(where: { (moleculeInfo) -> Bool in
            if equal(moleculeA: molecule, moleculeB: moleculeInfo.molecule) {
                return true
            } else if let parent = moleculeInfo.molecule as? ParentMoleculeModelProtocol {
                // Get all molecules of the same type for faster check.
                let molecules: [MoleculeModelProtocol] = parent.reduceDepthFirstTraverse(options: .childFirst, depth: 0, initialResult: []) { (accumulator, currentMolecule, depth) in
                    if currentMolecule.moleculeName == molecule.moleculeName {
                        return accumulator + [currentMolecule]
                    }
                    return accumulator
                }
                for moleculeB in molecules {
                    if equal(moleculeA: molecule, moleculeB: moleculeB) {
                        return true
                    }
                }
            }
            return false
        }) else { return }
        
        // Refresh the cell. (reload loses cell selection)
        let selectedIndex = tableView.indexPathForSelectedRow
        let indexPath = IndexPath(row: index, section: 0)
        tableView.reloadRows(at: [indexPath], with: .automatic)
        if let selectedIndex = selectedIndex {
            tableView.selectRow(at: selectedIndex, animated: false, scrollPosition: .none)
        }
    }
   
    //--------------------------------------------------
    // MARK: - Convenience
    //--------------------------------------------------
    
    /// Returns the (identifier, class) of the molecule for the given model.
    func createMoleculeInfo(with listItem: MoleculeModelProtocol?) -> MoleculeInfo? {
        
        guard let listItem = listItem,
              let moleculeClass = ModelRegistry.getMoleculeClass(listItem)
        else { return nil }
        
        let moleculeName = moleculeClass.nameForReuse(with: listItem, delegateObject() as? MVMCoreUIDelegateObject) ?? listItem.moleculeName
        
        return (moleculeName, moleculeClass, listItem)
    }
    
    /// Returns the (identifier, class) of the molecule for the indexPath.
    func getMoleculeInfo(for indexPath: IndexPath) -> MoleculeInfo? {
        moleculesInfo?[indexPath.row]
    }
    
    /// Sets up the molecule list and ensures no errors loading all content.
    func getMoleculeInfoList() -> [MoleculeInfo]? {
        
        var moleculeList: [MoleculeInfo] = []
        
        if let molecules = templateModel?.molecules {
            for molecule in molecules {
                if let info = createMoleculeInfo(with: molecule) {
                    moleculeList.append(info)
                }
            }
        }
        
        return moleculeList.count > 0 ? moleculeList : nil
    }
    
    /// Sets up the header, footer, molecule list and ensures no errors loading all content.
    func setup() {
        moleculesInfo = getMoleculeInfoList()
    }
    
    /// Adds modules from requiredModules() to the MVMCoreViewControllerMapping.requiredModules map.
    open func updateRequiredModules() {
        if let requiredModules = requiredModules(), let pageType = pageType {
            MVMCoreViewControllerMappingObject.shared()?.addRequiredModules(toMapping: requiredModules, forPageType: pageType)
        }
    }
    
    /// Gets modules required by the loadObject.pageJSON.
    open func requiredModules() -> [Any]? {
        var modules: [String]? = []
        var errors: [MVMCoreErrorObject]? = nil
        ModelRegistry.addRequiredModules(for: templateModel?.header, delegateObjectIVar, moduleList: &modules, errorList: &errors)
        ModelRegistry.addRequiredModules(for: templateModel?.footer, delegateObjectIVar, moduleList: &modules, errorList: &errors)
        
        if let molecules = templateModel?.molecules {
            for molecule in molecules {
                ModelRegistry.addRequiredModules(for: molecule, delegateObjectIVar, moduleList: &modules, errorList: &errors)
            }
        }
        
        return modules
    }
    
    /// Checks if the two molecules are equal
    private func equal(moleculeA: MoleculeModelProtocol, moleculeB: MoleculeModelProtocol) -> Bool {
        // TODO: move this to a better approach, maybe a UUID for each model.
        // Do instance check
        if let classMoleculeA = moleculeA as? NSObjectProtocol,
           let classMoleculeB = moleculeB as? NSObjectProtocol {
            return classMoleculeA === classMoleculeB
        }
        // Do json check
        return moleculeA.toJSON() == moleculeB.toJSON()
    }
}

extension MoleculeListTemplate: MoleculeListProtocol {
    open func removeMolecules(at indexPaths: [IndexPath], animation: UITableView.RowAnimation?) {
        for (index, indexPath) in indexPaths.sorted().enumerated() {
            let removeIndex = indexPath.row - index
            moleculesInfo?.remove(at: removeIndex)
        }

        guard let animation = animation,
              indexPaths.count > 0 else { return }
        tableView?.deleteRows(at: indexPaths, with: animation)
        updateViewConstraints()
        view.layoutIfNeeded()
    }
    
    open func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) {
        var indexPaths: [IndexPath] = []

        for molecule in molecules {
            if let info = self.createMoleculeInfo(with: molecule) {
                self.tableView?.register(info.class, forCellReuseIdentifier: info.identifier)
                let index = indexPath.row + indexPaths.count
                self.moleculesInfo?.insert(info, at: index)
                indexPaths.append(IndexPath(row: index, section: 0))
            }
        }

        guard let animation = animation,
              indexPaths.count > 0 else { return }
        self.tableView?.insertRows(at: indexPaths, with: animation)
        self.updateViewConstraints()
        self.view.layoutIfNeeded()
    }
    
    open func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? {
        guard let index = moleculesInfo?.firstIndex(where: { (moleculeInfo) -> Bool in
            return equal(moleculeA: molecule, moleculeB: moleculeInfo.molecule)
        }) else { return nil }
        
        return IndexPath(row: index, section: 0)
    }
}
