//
//  Toggle.swift
//  MVMCoreUI
//
//  Created by Kevin Christiano on 12/4/19.
//  Copyright © 2019 Verizon Wireless. All rights reserved.
//

import MVMCore
import UIKit

public typealias ActionBlockConfirmation = () -> (Bool)

/**
 A custom implementation of Apple's UISwitch.
 
 By default this class begins in the off state.
 
 Container: The background of the toggle control.
 Knob: The circular indicator that slides on the container.
 */
@objcMembers open class Toggle: Control, MVMCoreUIViewConstrainingProtocol {
    //--------------------------------------------------
    // MARK: - Properties
    //--------------------------------------------------
    
    /// Holds the on and off colors for the container.
    public var containerTintColor: (on: UIColor, off: UIColor) = (on: .mvmGreen, off: .mvmBlack)
    
    /// Holds the on and off colors for the knob.
    public var knobTintColor: (on: UIColor, off: UIColor) = (on: .mvmWhite, off: .mvmWhite)
    
    /// Holds the on and off colors for the disabled state..
    public var disabledTintColor: (container: UIColor, knob: UIColor) = (container: .mvmCoolGray3, knob: .mvmWhite)
    
    /// Set this flag to false if you do not want to animate state changes.
    public var isAnimated = true
    
    public var didToggleAction: ActionBlock?
    
    /// Executes logic before state change. If false, then toggle state will not change and the didToggleAction will not execute.
    public var shouldToggleAction: ActionBlockConfirmation? = {
        return { true }
    }()
    
    // Sizes are from InVision design specs.
    static let containerSize = CGSize(width: 51, height: 31)
    static let knobSize = CGSize(width: 28, height: 28)
    
    private var knobView: View = {
        let view = View()
        view.backgroundColor = .white
        view.layer.cornerRadius = Toggle.getKnobHeight() / 2.0
        return view
    }()
    
    //--------------------------------------------------
    // MARK: - Computed Properties
    //--------------------------------------------------
    
    open override var isEnabled: Bool {
        didSet {
            isUserInteractionEnabled = isEnabled
            changeStateNoAnimation(isEnabled ? isOn : false)
            setToggleAppearanceFromState()
            accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: isEnabled ? "AccToggleHint" : "AccDisabled")
        }
    }
        
    /// Simple means to prevent user interaction with the toggle.
    public var isLocked: Bool = false {
        didSet { isUserInteractionEnabled = !isLocked }
    }
    
    /// The state on the toggle. Default value: false.
    open var isOn: Bool = false {
        didSet {
            if isAnimated {
                UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: {
                    if self.isOn {
                        self.knobView.backgroundColor = self.knobTintColor.on
                        self.backgroundColor = self.containerTintColor.on
                        
                    } else {
                        self.knobView.backgroundColor = self.knobTintColor.off
                        self.backgroundColor = self.containerTintColor.off
                    }
                }, completion: nil)
                
                UIView.animate(withDuration: 0.33, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.2, options: [], animations: {
                    self.constrainKnob()
                    self.knobWidthConstraint?.constant = Self.getKnobWidth()
                    self.layoutIfNeeded()
                }, completion: nil)
                
            } else {
                setToggleAppearanceFromState()
                self.constrainKnob()
            }

            toggleModel?.selected = isOn
            _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
            accessibilityValue = isOn ? MVMCoreUIUtility.hardcodedString(withKey: "AccOn") : MVMCoreUIUtility.hardcodedString(withKey: "AccOff")
            setNeedsLayout()
            layoutIfNeeded()
        }
    }
    
    public var toggleModel: ToggleModel? {
        model as? ToggleModel
    }
    
    //--------------------------------------------------
    // MARK: - Delegate
    //--------------------------------------------------
    
    private var delegateObject: MVMCoreUIDelegateObject?
    
    //--------------------------------------------------
    // MARK: - Constraints
    //--------------------------------------------------
    
    private var knobLeadingConstraint: NSLayoutConstraint?
    private var knobTrailingConstraint: NSLayoutConstraint?
    private var knobHeightConstraint: NSLayoutConstraint?
    private var knobWidthConstraint: NSLayoutConstraint?
    private var heightConstraint: NSLayoutConstraint?
    private var widthConstraint: NSLayoutConstraint?
    
    private func constrainKnob() {
        
        knobLeadingConstraint?.isActive = false
        knobTrailingConstraint?.isActive = false
        
        _ = isOn ? constrainKnobOn() : constrainKnobOff()
        
        knobTrailingConstraint?.isActive = true
        knobLeadingConstraint?.isActive = true
    }
    
    private func constrainKnobOn() {
        
        knobTrailingConstraint = trailingAnchor.constraint(equalTo: knobView.trailingAnchor, constant: 2)
        knobLeadingConstraint = knobView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor)
    }
    
    private func constrainKnobOff() {
        
        knobTrailingConstraint = trailingAnchor.constraint(greaterThanOrEqualTo: knobView.trailingAnchor)
        knobLeadingConstraint = knobView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 2)
    }
    
    //--------------------------------------------------
    // MARK: - Initializers
    //--------------------------------------------------
    
    public override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    public convenience override init() {
        self.init(frame: .zero)
    }
    
    public convenience init(isOn: Bool) {
        self.init(frame: .zero)
        self.isOn = isOn
    }
    
    /// - parameter isOn: Bool to set the state of the toggle.
    /// - parameter didToggleAction: A closure which is executed after the toggle changes states.
    public convenience init(isOn: Bool = false, didToggleAction: ActionBlock?) {
        self.init(frame: .zero)
        changeStateNoAnimation(isOn)
        self.didToggleAction = didToggleAction
    }
    
    /// - parameter shouldToggleAction: Takes a closure that returns a boolean.
    /// - parameter didToggleAction: A closure which is executed after the toggle changes states.
    public convenience init(shouldToggleAction: ActionBlockConfirmation?, didToggleAction: ActionBlock?) {
        self.init(frame: .zero)
        self.didToggleAction = didToggleAction
        self.shouldToggleAction = shouldToggleAction
    }
    
    public required init?(coder: NSCoder) {
        super.init(coder: coder)
        fatalError("Toggle does not support xib.")
    }
    
    //--------------------------------------------------
    // MARK: - Lifecycle
    //--------------------------------------------------
    
    public override func updateView(_ size: CGFloat) {
        super.updateView(size)
        
        heightConstraint?.constant = Self.getContainerHeight()
        widthConstraint?.constant = Self.getContainerWidth()
        
        knobHeightConstraint?.constant = Self.getKnobHeight()
        knobWidthConstraint?.constant = Self.getKnobWidth()
        
        layer.cornerRadius = Self.getContainerHeight() / 2.0
        knobView.layer.cornerRadius = Self.getKnobHeight() / 2.0
        
        changeStateNoAnimation(isOn)
    }
    
    public override func setupView() {
        super.setupView()
 
        isAccessibilityElement = true
        accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "AccToggleHint")
        accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel")
        accessibilityTraits = .button

        heightConstraint = heightAnchor.constraint(equalToConstant: Self.containerSize.height)
        heightConstraint?.isActive = true
        
        widthConstraint = widthAnchor.constraint(equalToConstant: Self.containerSize.width)
        widthConstraint?.isActive = true
        
        layer.cornerRadius = Self.getContainerHeight() / 2.0
        backgroundColor = containerTintColor.off
        
        addSubview(knobView)
        
        knobHeightConstraint = knobView.heightAnchor.constraint(equalToConstant: Self.knobSize.height)
        knobHeightConstraint?.isActive = true
        knobWidthConstraint = knobView.widthAnchor.constraint(equalToConstant: Self.knobSize.width)
        knobWidthConstraint?.isActive = true
        knobView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        knobView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor).isActive = true
        bottomAnchor.constraint(greaterThanOrEqualTo: knobView.bottomAnchor).isActive = true
        
        constrainKnobOff()
    }
    
    public override func reset() {
        super.reset()
        
        backgroundColor = containerTintColor.off
        knobView.backgroundColor = knobTintColor.off
        accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel")
        isAnimated = true
        didToggleAction = nil
        shouldToggleAction = { return true }
    }
    
    class func getContainerWidth() -> CGFloat {
        let containerWidth = Self.containerSize.width
        return (MFSizeObject(standardSize: containerWidth, standardiPadPortraitSize: CGFloat(Self.containerSize.width * 1.5)))?.getValueBasedOnApplicationWidth() ?? containerWidth
    }
    
    class func getContainerHeight() -> CGFloat {
        let containerHeight = Self.containerSize.height
        return (MFSizeObject(standardSize: containerHeight, standardiPadPortraitSize: CGFloat(Self.containerSize.height * 1.5)))?.getValueBasedOnApplicationWidth() ?? containerHeight
    }
    
    class func getKnobWidth() -> CGFloat {
        let knobWidth = Self.knobSize.width
        return (MFSizeObject(standardSize: knobWidth, standardiPadPortraitSize: CGFloat(Self.knobSize.width * 1.5)))?.getValueBasedOnApplicationWidth() ?? knobWidth
    }
    
    class func getKnobHeight() -> CGFloat {
        let knobHeight = Self.knobSize.width
        return (MFSizeObject(standardSize: knobHeight, standardiPadPortraitSize: CGFloat(Self.knobSize.height * 1.5)))?.getValueBasedOnApplicationWidth() ?? knobHeight
    }
    
    //--------------------------------------------------
    // 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 Toggle and execute the actionBlock if provided.
    public func toggleAndAction() {
        
        if let result = shouldToggleAction?(), result {
            isOn.toggle()
            didToggleAction?()
        }
    }
    
    private func changeStateNoAnimation(_ state: Bool) {
        
        // Hold state in case User wanted isAnimated to remain off.
        let isAnimatedState = isAnimated
        
        isAnimated = false
        isOn = state
        isAnimated = isAnimatedState
    }
    
    override open func accessibilityActivate() -> Bool {
        // Hold state in case User wanted isAnimated to remain off.
        guard isUserInteractionEnabled else { return false }
        let isAnimatedState = isAnimated
        isAnimated = false
        sendActions(for: .touchUpInside)
        isAnimated = isAnimatedState
        return true
    }
    
    //--------------------------------------------------
    // MARK: - UIResponder
    //--------------------------------------------------
    
    open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        UIView.animate(withDuration: 0.1, animations: {
            self.knobWidthConstraint?.constant += PaddingOne
            self.layoutIfNeeded()
        })
    }
    
    public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        knobReformAnimation()
        
        // Action only occurs of the user lifts up from withing acceptable region of the toggle.
        guard let coordinates = touches.first?.location(in: self),
            coordinates.x > -20,
            coordinates.x < bounds.width + 20,
            coordinates.y > -20,
            coordinates.y < bounds.height + 20
            else { return }
        
        sendActions(for: .touchUpInside)
    }
    
    public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
        
        knobReformAnimation()
        sendActions(for: .touchCancel)
    }
    
    //--------------------------------------------------
    // MARK: - Animations
    //--------------------------------------------------
    
    public func setToggleAppearanceFromState() {
        
        backgroundColor = isEnabled ? isOn ? containerTintColor.on : containerTintColor.off : disabledTintColor.container
        knobView.backgroundColor = isEnabled ? isOn ? knobTintColor.on : knobTintColor.off : disabledTintColor.knob
    }
    
    public func knobReformAnimation() {
        
        if isAnimated {
            UIView.animate(withDuration: 0.1, animations: {
                self.knobWidthConstraint?.constant = Self.getKnobWidth()
                self.layoutIfNeeded()
            }, completion: nil)
            
        } else {
            knobWidthConstraint?.constant = Self.getKnobWidth()
            layoutIfNeeded()
        }
    }

    // MARK:- MoleculeViewProtocol
    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? ToggleModel else { return }
        
        FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
        
        containerTintColor.on = model.onTintColor.uiColor
        containerTintColor.off = model.offTintColor.uiColor
        knobTintColor.on = model.onKnobTintColor.uiColor
        knobTintColor.off = model.offKnobTintColor.uiColor
        isOn = model.selected
        changeStateNoAnimation(isOn)
        isAnimated = model.animated
        isEnabled = model.enabled && !model.readOnly
        
        if let accessibileString = model.accessibilityText {
            accessibilityLabel = accessibileString
        }
        
        let additionalDataWithSource = additionalData.dictionaryAdding(key: KeySourceModel, value: model)
        if model.action != nil || model.alternateAction != nil {
            didToggleAction = { [weak self] in
                guard let self = self else { return }
                if self.isOn {
                    if let action = model.action {
                        MVMCoreActionHandler.shared()?.asyncHandleAction(with: action, additionalData: additionalDataWithSource, delegateObject: delegateObject)
                    }
                } else {
                    if let action = model.alternateAction ?? model.action {
                        MVMCoreActionHandler.shared()?.asyncHandleAction(with: action, additionalData: additionalDataWithSource, delegateObject: delegateObject)
                    }
                }
            }
        }
    }
    
    public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
        Self.getContainerHeight()
    }
}

// MARK: - MVMCoreUIViewConstrainingProtocol
extension Toggle {
    
    public func needsToBeConstrained() -> Bool { true }
    
    public func horizontalAlignment() -> UIStackView.Alignment { .trailing }
}
