//
//  Checkbox.swift
//  MVMCoreUI
//
//  Created by Kevin Christiano on 9/13/19.
//  Copyright © 2019 Verizon Wireless. All rights reserved.
//

import MVMCore

/**
 This class expects its height and width to be equal.
 */
@objcMembers open class Checkbox: Control, MVMCoreUIViewConstrainingProtocol {
    //--------------------------------------------------
    // MARK: - Properties
    //--------------------------------------------------
    
    public var sizeObject: MFSizeObject? = MFSizeObject(standardSize: Checkbox.defaultHeightWidth, standardiPadPortraitSize: Checkbox.defaultHeightWidth + 6.0)
    
    // Form Validation
    var fieldKey: String?
    var fieldValue: JSONValue?
    var groupName: String?
    var delegateObject: MVMCoreUIDelegateObject?
    
    public var checkboxModel: CheckboxModel? {
        model as? CheckboxModel
    }
    
    public static let defaultHeightWidth: CGFloat = 18.0
    
    /// If true the border of this checkbox will be circular.
    public var isRound: Bool = false
    
    /// Determined if the checkbox's UI should animated when selected.
    public var isAnimated: Bool = true
    
    /// Disables all selection logic when setting the value of isSelected, reducing it to a stored property.
    public var updateSelectionOnly: Bool = false
    
    /// The color of the background when checked.
    public var checkedBackgroundColor: UIColor = .clear {
        didSet {
            if isSelected {
                backgroundColor = checkedBackgroundColor
            }
        }
    }
    
    /// The color of the background when unChecked.
    public var unCheckedBackgroundColor: UIColor = .clear {
        didSet {
            if !isSelected {
                backgroundColor = unCheckedBackgroundColor
            }
        }
    }
    
    /// Retrieves ideeal radius value to curve square into a circle.
    public var cornerRadiusValue: CGFloat {
        bounds.size.height / 2
    }
    
    /// Action Block called when the switch is selected.
    public var actionBlock: ActionBlock?
    
    /// Manages the appearance of the checkbox.
    private var shapeLayer: CAShapeLayer?
    
    /// Width of the check mark.
    public var checkWidth: CGFloat = 2 {
        didSet {
            if let shapeLayer = shapeLayer {
                CATransaction.withDisabledAnimations {
                    shapeLayer.lineWidth = checkWidth
                }
            }
        }
    }
    
    open override var isEnabled: Bool {
        didSet {
            
            isUserInteractionEnabled = isEnabled
            
            if isEnabled {
                layer.borderColor = borderColor.cgColor
                backgroundColor = isSelected ? checkedBackgroundColor : unCheckedBackgroundColor
                setShapeLayerStrokeColor(checkColor)
            } else {
                layer.borderColor = disabledBorderColor.cgColor
                backgroundColor = disabledBackgroundColor
                setShapeLayerStrokeColor(disabledCheckColor)
            }
        }
    }

    public var disabledBackgroundColor: UIColor = .clear
    public var disabledBorderColor: UIColor = .mvmCoolGray3
    public var disabledCheckColor: UIColor = .mvmCoolGray3
    
    /// Color of the check mark.
    public var checkColor: UIColor = .mvmBlack {
        didSet { setShapeLayerStrokeColor(checkColor) }
    }
    
    /// Border width of the checkbox
    public var borderWidth: CGFloat = 1 {
        didSet { layer.borderWidth = borderWidth }
    }
    
    /// border color of the Checkbox
    public var borderColor: UIColor = .mvmBlack {
        didSet { layer.borderColor = borderColor.cgColor }
    }
    
    /**
     The represented state of the Checkbox.
     
     Setting updateSelectionOnly to true bypasses the animation logic inherent with setting this property.
     */
    override open var isSelected: Bool {
        didSet {
            if !updateSelectionOnly {
                layoutIfNeeded()
                (model as? CheckboxModel)?.selected = isSelected
                shapeLayer?.removeAllAnimations()
                updateCheckboxUI(isSelected: isSelected, isAnimated: isAnimated)                
                _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
                updateAccessibilityLabel()
            }
        }
    }
    
    //--------------------------------------------------
    // MARK: - Constraints
    //--------------------------------------------------
    
    public var heightConstraint: NSLayoutConstraint?
    public var widthConstraint: NSLayoutConstraint?
    
    /// Updates the height and width anchors of the Checkbox with the assigned value.
    public var heigthWidthConstant: CGFloat = Checkbox.defaultHeightWidth {
        didSet {
            heightConstraint?.constant = heigthWidthConstant
            widthConstraint?.constant = heigthWidthConstant
        }
    }
    
    //--------------------------------------------------
    // MARK: - Initializers
    //--------------------------------------------------
    
    override public init(frame: CGRect) {
        super.init(frame: frame)
        
        isAccessibilityElement = true
        accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "checkbox_action_hint")
        accessibilityTraits = .button
        updateAccessibilityLabel()
    }
    
    /// There is currently no intention on using xib files.
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        fatalError("xib file is not implemented for Checkbox.")
    }
    
    public convenience override init() {
        self.init(frame:.zero)
    }
    
    public convenience init(isChecked: Bool) {
        self.init(frame: .zero)
        checkAndBypassAnimations(selected: isChecked)
    }
    
    public convenience init(checkedBackgroundColor: UIColor, unCheckedBackgroundColor: UIColor, isChecked: Bool = false) {
        self.init(frame: .zero)
        checkAndBypassAnimations(selected: isChecked)
        self.checkedBackgroundColor = checkedBackgroundColor
        self.unCheckedBackgroundColor = unCheckedBackgroundColor
    }
    
    //--------------------------------------------------
    // MARK: - Lifecycle
    //--------------------------------------------------
    
    override open func layoutSubviews() {
        super.layoutSubviews()
        
        drawShapeLayer()
        layer.cornerRadius = isRound ? cornerRadiusValue : 0
    }
    
    open override func setupView() {
        super.setupView()
        
        isUserInteractionEnabled = true
        translatesAutoresizingMaskIntoConstraints = false
        backgroundColor = .clear
        
        widthConstraint = widthAnchor.constraint(equalToConstant: Checkbox.defaultHeightWidth)
        heightConstraint = heightAnchor.constraint(equalToConstant: Checkbox.defaultHeightWidth)
        heightWidthIsActive(true)
    }
    
    //--------------------------------------------------
    // MARK: - Actions
    //--------------------------------------------------
    
    open override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
        super.sendAction(action, to: target, for: event)
        toggleAndAction()
    }
    
    open override func sendActions(for controlEvents: UIControl.Event) {
        super.sendActions(for: controlEvents)
        toggleAndAction()
    }
    
    /// This will toggle the state of the Checkbox and execute the actionBlock if provided.
    public func toggleAndAction() {
        isSelected.toggle()
        actionBlock?()
    }
    
    //--------------------------------------------------
    // MARK: - Methods
    //--------------------------------------------------
    
    /// Creates the check mark layer.
    private func drawShapeLayer() {
        
        if shapeLayer == nil {
            
            let shapeLayer = CAShapeLayer()
            self.shapeLayer = shapeLayer
            shapeLayer.frame = bounds
            layer.addSublayer(shapeLayer)
            shapeLayer.strokeColor = isEnabled ? checkColor.cgColor : disabledCheckColor.cgColor
            shapeLayer.fillColor = UIColor.clear.cgColor
            shapeLayer.path = checkMarkPath()
            shapeLayer.lineJoin = .miter
            shapeLayer.lineWidth = checkWidth
            
            CATransaction.withDisabledAnimations {
                shapeLayer.strokeEnd = isSelected ? 1 : 0
            }
        }
    }
    
    /// - returns: The CGPath of a UIBezierPath detailing the path of a checkmark
    func checkMarkPath() -> CGPath {
        
        let length = max(bounds.size.height, bounds.size.width)
        let xInsetLeft = length * 0.25
        let yInsetTop = length * 0.3
        let innerWidth = length - (xInsetLeft + length * 0.25) // + Right X Inset
        let innerHeight = length - (yInsetTop + length * 0.35) // + Bottom Y Inset
        
        let startPoint = CGPoint(x: xInsetLeft, y: yInsetTop + (innerHeight / 2))
        let pivotOffSet = CGPoint(x: xInsetLeft + (innerWidth * 0.33), y: yInsetTop + innerHeight)
        let endOffset = CGPoint(x: xInsetLeft + innerWidth, y: yInsetTop)
        
        let bezierPath = UIBezierPath()
        bezierPath.move(to: startPoint)
        bezierPath.addLine(to: pivotOffSet)
        bezierPath.addLine(to: endOffset)
        
        return bezierPath.cgPath
    }
    
    ///  Programmatic means to check/uncheck the box.
    /// - parameter selected: state of the check box: true = checked OR false = unchecked.
    /// - parameter animated: allows the state of the checkbox to change with or without animation.
    public func updateSelection(to selected: Bool, animated: Bool) {
        
        DispatchQueue.main.async {
            
            self.checkAndBypassAnimations(selected: selected)
            self.drawShapeLayer()
            self.shapeLayer?.removeAllAnimations()
            self.updateCheckboxUI(isSelected: selected, isAnimated: animated)
        }
    }
    
    /// updates the visuals of the check mark and background.
    /// - parameter isSelected: the check state of the checkbox.
    /// - parameter isAnimated: determines of the changes should animate or immediately refelect.
    public func updateCheckboxUI(isSelected: Bool, isAnimated: Bool) {
        
        if isAnimated {
            let animateStrokeEnd = CABasicAnimation(keyPath: "strokeEnd")
            animateStrokeEnd.timingFunction = CAMediaTimingFunction(name: .linear)
            animateStrokeEnd.duration = 0.3
            animateStrokeEnd.fillMode = .both
            animateStrokeEnd.isRemovedOnCompletion = false
            animateStrokeEnd.fromValue = !isSelected ? 1 : 0
            animateStrokeEnd.toValue = isSelected ? 1 : 0
            self.shapeLayer?.add(animateStrokeEnd, forKey: "strokeEnd")
            
            UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseOut, animations: {
                self.backgroundColor = isSelected ? self.checkedBackgroundColor : self.unCheckedBackgroundColor
            })
        } else {
            CATransaction.withDisabledAnimations {
                self.shapeLayer?.strokeEnd = isSelected ? 1 : 0
            }
            
            backgroundColor = isSelected ? checkedBackgroundColor : unCheckedBackgroundColor
        }
    }
    
    /// Adjust accessibility label based on state of Checkbox.
    func updateAccessibilityLabel() {
        // Attention: This needs to be addressed with the accessibility team.
        // NOTE: Currently emptying description part of MVMCoreUICheckBox accessibility label to avoid crashing!
        if let state = MVMCoreUIUtility.hardcodedString(withKey: isSelected ? "checkbox_checked_state" : "checkbox_unchecked_state") {
            accessibilityLabel = String(format: MVMCoreUIUtility.hardcodedString(withKey: "checkbox_desc_state") ?? "%@%@", "", state)
        }
    }
    
    private func setShapeLayerStrokeColor(_ color: UIColor) {
        
        if let shapeLayer = shapeLayer {
            CATransaction.withDisabledAnimations {
                shapeLayer.strokeColor = color.cgColor
            }
        }
    }
    
    public func heightWidthIsActive(_ isActive: Bool) {
        
        heightConstraint?.isActive = isActive
        widthConstraint?.isActive = isActive
    }
    
    private func checkAndBypassAnimations(selected: Bool) {
        updateSelectionOnly = true
        isSelected = selected
        updateSelectionOnly = false
    }
    
    //--------------------------------------------------
    // MARK: - UITouch
    //--------------------------------------------------
    
    open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        sendActions(for: .touchUpInside)
    }
    
    override open func accessibilityActivate() -> Bool {
        guard isEnabled else { return false }
        sendActions(for: .touchUpInside)
        return true
    }
    
    //--------------------------------------------------
    // MARK: - Molecular
    //--------------------------------------------------
    
    open func needsToBeConstrained() -> Bool { true }
    
    open override func reset() {
        super.reset()
        
        isEnabled = true
        shapeLayer?.removeAllAnimations()
        shapeLayer?.removeFromSuperlayer()
        shapeLayer = nil
        backgroundColor = .clear
        borderColor = .mvmBlack
        borderWidth = 1
        checkColor = .mvmBlack
        checkWidth = 2
        checkAndBypassAnimations(selected: false)
    }
    
    public override func updateView(_ size: CGFloat) {
        super.updateView(size)
        
        if let dimension = sizeObject?.getValueBased(onSize: size) {
            widthConstraint?.constant = dimension
            heightConstraint?.constant = dimension
        }
    }
    
    private func performCheckboxAction(with actionModel: ActionModelProtocol, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
        var additionalDataToUpdate = additionalData ?? [:]
        additionalDataToUpdate[KeySourceModel] = checkboxModel
        MVMCoreActionHandler.shared()?.asyncHandleAction(with: actionModel, additionalData: additionalDataToUpdate, delegateObject: delegateObject)
    }
    
    public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
        super.set(with: model, delegateObject, additionalData)
        self.delegateObject = delegateObject
        
        guard let model = model as? CheckboxModel else { return }
        
        FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
        
        if let fieldKey = model.fieldKey {
            self.fieldKey = fieldKey
        }
        
        borderColor = (model.inverted ? model.invertedColor : model.borderColor).uiColor
        borderWidth = model.borderWidth
        
        checkColor = (model.inverted ? model.invertedColor : model.checkColor).uiColor
        unCheckedBackgroundColor = (model.inverted ? model.invertedBackgroundColor : model.unCheckedBackgroundColor).uiColor
        checkedBackgroundColor = (model.inverted ? model.invertedBackgroundColor : model.checkedBackgroundColor).uiColor
        disabledCheckColor = (model.inverted ? model.invertedColor : model.disabledCheckColor).uiColor
        disabledBorderColor = (model.inverted ? model.invertedColor : model.disabledBorderColor).uiColor
        disabledBackgroundColor = (model.inverted ? model.invertedColor : model.disabledBackgroundColor).uiColor
        
        isAnimated = model.animated
        isRound = model.round
        
        if model.selected {
            checkAndBypassAnimations(selected: model.selected)
        }
        
        model.updateUI = { [weak self] in
            MVMCoreDispatchUtility.performBlock(onMainThread: {
                guard let self = self else { return }
                self.isEnabled = model.enabled
            })
        }
        
        isEnabled = model.enabled && !model.readOnly
        
        if (model.action != nil || model.offAction != nil) {
            actionBlock = { [weak self] in
                guard let self = self else { return }
                
                if let offAction = model.offAction, !self.isSelected  {
                    self.performCheckboxAction(with: offAction, delegateObject: delegateObject, additionalData: additionalData)
                    
                } else if let action = model.action {
                    self.performCheckboxAction(with: action, delegateObject: delegateObject, additionalData: additionalData)
                }
            }
        }
    }
}
