//
//  Stack.swift
//  MVMCoreUI
//
//  Created by Scott Pfeil on 1/16/20.
//  Copyright © 2020 Verizon Wireless. All rights reserved.
//


open class Stack<T>: Container where T: (StackModelProtocol & MoleculeModelProtocol) {
    //--------------------------------------------------
    // MARK: - Properties
    //--------------------------------------------------
    
    open var contentView: UIView = MVMCoreUICommonViewsUtility.commonView()
    open var stackItems: [UIView] = []
    
    open var stackModel: T? {
        model as? T
    }
    
    //--------------------------------------------------
    // MARK: - Helpers
    //--------------------------------------------------
    
    open func pinView(_ view: UIView, toView: UIView, attribute: NSLayoutConstraint.Attribute, relation: NSLayoutConstraint.Relation, priority: UILayoutPriority, constant: CGFloat) {
        
        let constraint = NSLayoutConstraint(item: view, attribute: attribute, relatedBy: relation, toItem: toView, attribute: attribute, multiplier: 1.0, constant: constant)
        constraint.priority = priority
        constraint.isActive = true
    }
    
    /// Restacks the existing items.
    open func restack() {
        
        removeAllItemViews()
        
        guard let stackModel = stackModel else { return }
        
        let stackItems = self.stackItems
        self.stackItems = []
        let lastItemIndex = stackModel.molecules.lastIndex { !$0.gone }
        
        // Adds the views
        let totalSpace = getTotalSpace()
        for (index, view) in stackItems.enumerated() {
            addView(view, stackModel.molecules[index], totalSpacing: totalSpace, lastItem: lastItemIndex == index)
        }
        
        isAccessibilityElement = false
        var accessibleViews: [Any] = []
        
        for (index, view) in stackItems.enumerated() where !stackModel.molecules[index].gone {
            accessibleViews.append(view)
        }
        
        accessibilityElements = accessibleViews
    }
    
    /// Removes all stack items views from the view.
    open func removeAllItemViews() {
        stackItems.forEach { $0.removeFromSuperview() }
    }
    
    /// A convenience function for when the stackItems are containers and we want to update them based on the contained molecules models. If model is nil, stackItem is set to gone. Restacks if necessary.
    open func updateContainedMolecules(with models: [MoleculeModelProtocol?], _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
        
        guard var stackModel = stackModel else { return }
        var needsRestack = false
        
        for (index, item) in stackItems.enumerated() {
            guard let container = item as? UIView & ContainerProtocol,
                let contained = container.view as? MoleculeViewProtocol
                else { continue }
            
            if let model = models[index] {
                contained.set(with: model, delegateObject, additionalData)
                if stackModel.molecules[index].gone {
                    stackModel.molecules[index].gone = false
                    needsRestack = true
                }
            } else if !stackModel.molecules[index].gone {
                stackModel.molecules[index].gone = true
                needsRestack = true
            }
        }
        
        if needsRestack {
            restack()
        }
    }
    
    //--------------------------------------------------
    // MARK: - Initializers
    //--------------------------------------------------
    
    public override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    /// The main initializer for model driven
    public init(with model: T, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
        super.init(frame: .zero)
        setOptional(with: model, delegateObject, additionalData)
    }
    
    /// The main initializer for hardcode driven
    public init(with model: T, stackItems: [UIView]) {
        super.init(frame: CGRect.zero)
        self.model = model
        self.stackItems = stackItems
    }
    
    public required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    public required init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
        super.init(frame: .zero)
        set(with: model, delegateObject, additionalData)
    }
    
    /// Returns a Stack created with a StackModel and StackItems containing the passed in views.
    public static func createStack(with views: [UIView], axis: NSLayoutConstraint.Axis? = nil, spacing: CGFloat? = nil) -> Stack<StackModel> {
        
        var items: [StackItem] = []
        var models: [StackItemModel] = []
        
        for view in views {
            items.append(StackItem(andContain: view))
            models.append(StackItemModel())
        }
        
        let model = StackModel(molecules: models, axis: axis, spacing: spacing)
        return Stack<StackModel>(with: model, stackItems: items)
    }
    
    /// Returns a Stack created with a StackModel containing the passed in views and using the passed in stackitems.
    public static func createStack(with viewModels:[(view: UIView, model: StackItemModel)], axis: NSLayoutConstraint.Axis? = nil, spacing: CGFloat? = nil) -> Stack<StackModel> {
        var stackItems: [StackItem] = []
        var models: [StackItemModel] = []
        
        for item in viewModels {
            stackItems.append(StackItem(andContain: item.view))
            models.append(item.model)
        }
        
        let model = StackModel(molecules: models, axis: axis, spacing: spacing)
        return Stack<StackModel>(with: model, stackItems: stackItems)
    }
    
    //--------------------------------------------------
    // MARK: - MFViewProtocol
    //--------------------------------------------------
    
    open override func setupView() {
        super.setupView()
        
        guard contentView.superview == nil else { return }
        
        MVMCoreUIUtility.setMarginsFor(contentView, leading: 0, top: 0, trailing: 0, bottom: 0)
        translatesAutoresizingMaskIntoConstraints = false
        backgroundColor = .clear
        addSubview(contentView)
        containerHelper.constrainView(contentView)
        contentView.setContentHuggingPriority(.defaultHigh, for: .vertical)
        contentView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
    }
    
    open override func updateView(_ size: CGFloat) {
        super.updateView(size)
        for item in stackItems {
            (item as? MVMCoreViewProtocol)?.updateView(size)
        }
    }
    
    //--------------------------------------------------
    // MARK: - MoleculeViewProtocol
    //--------------------------------------------------
    
    open override func reset() {
        super.reset()
        backgroundColor = .clear
        for item in stackItems {
            (item as? MoleculeViewProtocol)?.reset()
        }
    }
    
    open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
        super.set(with: model, delegateObject, additionalData)
        setStackItemsFromModel(model, delegateObject, additionalData)
        restack()
    }
    
    open override class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? {
        // This will aggregate names of molecules to make an id.
        guard let model = model as? T else {
            return "stack<>"
        }
        var name = "stack<"
        for case let item in model.molecules {
            if let moleculeClass = ModelRegistry.getMoleculeClass(item),
                let nameForReuse = moleculeClass.nameForReuse(with: item, delegateObject) {
                name.append(nameForReuse + ",")
            } else {
                name.append(item.moleculeName + ",")
            }
        }
        name.append(">")
        return name
    }
    
    // Need to update to take into account first spacing flag
    open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
        guard let model = model as? T else { return 0 }
        let horizontal = model.axis == .horizontal
        var estimatedHeight: CGFloat = 0
        
        for case let item in model.molecules {
            if item.gone { continue }
            let height = (ModelRegistry.getMoleculeClass(item))?.estimatedHeight(with: item, delegateObject) ?? 0
            if !horizontal {
                // Vertical stack aggregates the items
                let spacing = item.spacing ?? model.spacing
                estimatedHeight += (height + spacing)
            } else {
                // Horizontal stack takes the tallest item.
                estimatedHeight = max(estimatedHeight, height)
            }
        }
        return estimatedHeight
    }
    
    open override class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>?) -> [String]? {
        
        guard let model = model as? T else { return nil }
        var modules: [String] = []
        
        for case let item in model.molecules {
            if let modulesForMolecule = (ModelRegistry.getMoleculeClass(item))?.requiredModules(with: item, delegateObject, error: error) {
                modules += modulesForMolecule
            }
        }
        
        return modules.count > 0 ? modules : nil
    }
    
    //--------------------------------------------------
    // MARK: - Subclassables
    //--------------------------------------------------
    
    /// Can be subclassed to set stack items with model when we already have views
    open func setStackItemsFromModel(_ model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
        
        guard let models = stackModel?.molecules else { return }
        
        for (index, element) in models.enumerated() {
            (stackItems[index] as? MoleculeViewProtocol)?.set(with: element, delegateObject, additionalData)
        }
    }
    
    //--------------------------------------------------
    // MARK: - Adding to stack
    //--------------------------------------------------
    /// Sets the stack with StackItems containing the passed in views and creates a StackModel with StackItems.
    open func setAndCreateModel(with views: [UIView]) {
        
        var stackItems: [StackItem] = []
        var models: [StackItemModel] = []
        
        for view in views {
            stackItems.append(StackItem(andContain: view))
            models.append(StackItemModel())
        }
        
        self.stackItems = stackItems
        model = StackModel(molecules: models)
    }
    
    /// Sets the stack with StackItems containing the passed in views and sets the StackModel with models.
    open func set(with viewModels:[(view: UIView, model: StackItemModel)]) {
        
        guard var stackModel = self.stackModel else { return }
        
        var stackItems: [StackItem] = []
        var models: [StackItemModel] = []
        
        for item in viewModels {
            stackItems.append(StackItem(andContain: item.view))
            models.append(item.model)
        }
        
        stackModel.molecules = models
        self.stackItems = stackItems
    }
    
    /// Gets the percent modifier. This value is used to help properly calculate percent for stack items when spacing is involved.
    private func getTotalSpace() -> CGFloat {
        
        guard let stackModel = stackModel else { return 0.0 }
        var totalSpace: CGFloat = 0.0
        var firstMoleculeFound = false
        
        for stackItemModel in stackModel.molecules {
            guard !stackItemModel.gone else { continue }
            let spacing = stackItemModel.spacing ?? stackModel.spacing
            if firstMoleculeFound {
                totalSpace += spacing
            } else {
                firstMoleculeFound = true
                totalSpace += (stackModel.useStackSpacingBeforeFirstItem ? spacing : stackItemModel.spacing ?? 0)
            }
        }
        
        return totalSpace
    }
    
    /// Adds the stack item view
    private func addView(_ view: UIView,_ model: StackItemModelProtocol, totalSpacing: CGFloat, lastItem: Bool) {
        
        guard let stackModel = self.stackModel else { return }
        guard !model.gone else {
            // Gone views do not show
            stackItems.append(view)
            return
        }
        contentView.addSubview(view)
        view.translatesAutoresizingMaskIntoConstraints = false
        
        let spacing = model.spacing ?? stackModel.spacing
        if let container = view as? ContainerProtocol {
            let verticalAlignment = (model as? ContainerModelProtocol)?.verticalAlignment ?? (container.view as? MVMCoreUIViewConstrainingProtocol)?.verticalAlignment?() ?? (model.percent == nil && stackModel.axis == .vertical ? .fill : (stackModel.axis == .vertical ? .leading : .center))
            let horizontalAlignment = (model as? ContainerModelProtocol)?.horizontalAlignment ?? (container.view as? MVMCoreUIViewConstrainingProtocol)?.horizontalAlignment?() ?? (stackModel.axis == .vertical || model.percent == nil ? .fill : .leading)
            container.alignHorizontal(horizontalAlignment)
            container.alignVertical(verticalAlignment)
        }
        
        let first = contentView.subviews.count == 1
        if stackModel.axis == .vertical {
            if first {
                pinView(view, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: stackModel.useStackSpacingBeforeFirstItem ? spacing : model.spacing ?? 0)
            } else if let previousView = stackItems.last(where: { item in
                return item.superview != nil
            }) {
                view.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: spacing).isActive = true
            }
            pinView(view, toView: contentView, attribute: .leading, relation: .equal, priority: .required, constant: 0)
            pinView(contentView, toView: view, attribute: .trailing, relation: .equal, priority: .required, constant: 0)
            if let percent = model.percent {
                let multiplier = CGFloat(percent) / 100.0
                let constant = multiplier * totalSpacing
                view.heightAnchor.constraint(equalTo: heightAnchor, multiplier: multiplier, constant: -constant).isActive = true
            }
            if lastItem {
                pinView(contentView, toView: view, attribute: .bottom, relation: .equal, priority: .required, constant: 0)
            }
        } else {
            if first {
                // First horizontal item has no spacing by default unless told otherwise.
                pinView(view, toView: contentView, attribute: .leading, relation: .equal, priority: .required, constant: stackModel.useStackSpacingBeforeFirstItem ? spacing : model.spacing ?? 0)
            } else if let previousView = stackItems.last(where: { item in
                return item.superview != nil
            }) {
                view.leftAnchor.constraint(equalTo: previousView.rightAnchor, constant: spacing).isActive = true
            }
            pinView(view, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: 0)
            pinView(contentView, toView: view, attribute: .bottom, relation: .equal, priority: .required, constant: 0)
            if let percent = model.percent {
                let multiplier = CGFloat(percent) / 100.0
                let constant = multiplier * totalSpacing
                view.widthAnchor.constraint(equalTo: widthAnchor, multiplier: multiplier, constant: -constant).isActive = true
            }
            if lastItem {
                pinView(contentView, toView: view, attribute: .right, relation: .equal, priority: .required, constant: 0)
            }
        }
        
        stackItems.append(view)
    }
}
