//
//  LoadingSpinner.swift
//  MVMCoreUI
//
//  Created by Kevin Christiano on 5/20/20.
//  Copyright © 2020 Verizon Wireless. All rights reserved.
//

import UIKit


open class LoadingSpinner: View {
    //--------------------------------------------------
    // MARK: - Properties
    //--------------------------------------------------
    
    public var strokeColor: UIColor = .mvmBlack
    
    public var lineWidth: CGFloat = 4.0
    
    public var speed: Float = 1.5
    
    //--------------------------------------------------
    // MARK: - Constraints
    //--------------------------------------------------
    
    public var heightConstraint: NSLayoutConstraint?
    public var widthConstraint: NSLayoutConstraint?
    
    //--------------------------------------------------
    // MARK: - Lifecycle
    //--------------------------------------------------
    
    override open var layer: CAShapeLayer {
        get { return super.layer as! CAShapeLayer }
    }
    
    override open class var layerClass: AnyClass {
        return CAShapeLayer.self
    }
    
    open override func setupView() {
        super.setupView()
        
        heightConstraint = heightAnchor.constraint(equalToConstant: 0)
        widthConstraint = widthAnchor.constraint(equalToConstant: 0)
    }
    
    override open func layoutSubviews() {
        super.layoutSubviews()

        layer.fillColor = nil
        layer.strokeColor = strokeColor.cgColor
        layer.lineWidth = lineWidth
        layer.lineCap = .butt
        layer.speed = speed
        let halfWidth = lineWidth / 2
        let radius = (bounds.width - lineWidth) / 2
        layer.path = UIBezierPath(arcCenter: CGPoint(x: radius + halfWidth,
                                                     y: radius + halfWidth),
                                  radius: radius,
                                  startAngle: -CGFloat.pi / 2,
                                  endAngle: 2 * CGFloat.pi,
                                  clockwise: true).cgPath
    }
    
    open override func updateView(_ size: CGFloat) {
        super.updateView(size)

        layer.removeAllAnimations()
        animate()
    }
    
    public override func reset() {
        super.reset()
        
        layer.removeAllAnimations()
        heightConstraint?.isActive = false
        widthConstraint?.isActive = false
    }
    
    //--------------------------------------------------
    // MARK: - Animation
    //--------------------------------------------------
    
    override open func didMoveToWindow() {
        animate()
    }
    
    struct Pose {
        /// Delayed time (in seconds) to execute after the previous Pose.
        let delay: CFTimeInterval
        /// The time into the animation to begin drawing.
        let startTime: CGFloat
        /// The length of the drawn line.
        let length: CGFloat
    }
    
    // TODO: This needs more attention to improve frame smoothness.
    class var poses: [Pose] {
        get {
            return [
                Pose(delay: 0.0, startTime: 0.000, length: 0.7),
                Pose(delay: 0.7, startTime: 0.500, length: 0.5),
                Pose(delay: 0.6, startTime: 1.000, length: 0.3),
                Pose(delay: 0.5, startTime: 1.500, length: 0.2),
                Pose(delay: 0.5, startTime: 1.875, length: 0.2),
                Pose(delay: 0.3, startTime: 2.250, length: 0.3),
                Pose(delay: 0.2, startTime: 2.600, length: 0.5),
                Pose(delay: 0.2, startTime: 3.000, length: 0.7)
            ]
        }
    }
    
    private func animate() {
        var time: CFTimeInterval = 0
        var times = [CFTimeInterval]()
        var start: CGFloat = 0
        var rotations = [CGFloat]()
        var strokeEnds = [CGFloat]()
        
        let poses = Self.poses
        var totalSeconds: CFTimeInterval = poses.reduce(0) { $0 + $1.delay }
        
        for pose in poses {
            time += pose.delay
            times.append(time / totalSeconds)
            start = pose.startTime
            rotations.append(start * 2 * CGFloat.pi)
            strokeEnds.append(pose.length)
        }
        
        totalSeconds += 0.3
        
        animateKeyPath(keyPath: "strokeEnd", duration: totalSeconds, times: times, values: strokeEnds)
        animateKeyPath(keyPath: "transform.rotation", duration: totalSeconds, times: times, values: rotations)
    }
    
    private func animateKeyPath(keyPath: String, duration: CFTimeInterval, times: [CFTimeInterval], values: [CGFloat]) {
        
        let animation = CAKeyframeAnimation(keyPath: keyPath)
        animation.keyTimes = times as [NSNumber]?
        animation.values = values
        animation.calculationMode = .linear
        animation.timingFunction = CAMediaTimingFunction(name: .linear)
        animation.duration = duration
        animation.rotationMode = .rotateAuto
        animation.fillMode = .forwards
        animation.isRemovedOnCompletion = false
        animation.repeatCount = .infinity
        layer.add(animation, forKey: animation.keyPath)
    }
    
    //--------------------------------------------------
    // MARK: - Methods
    //--------------------------------------------------
    
    func resumeSpinnerAfterDelay() {
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
            self?.resumeAnimations()
        }
    }
    
    func pauseAnimations() {
        
        let pausedTime = layer.convertTime(CACurrentMediaTime(), from: nil)
        layer.speed = 0
        isHidden = true
        layer.timeOffset = pausedTime
    }
    
    func resumeAnimations() {
        
        let pausedTime = layer.timeOffset
        isHidden = false
        layer.speed = speed
        layer.timeOffset = 0
        let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
        layer.beginTime = timeSincePause
    }
    
    func stopAllAnimations() {
        
        layer.removeAllAnimations()
    }
    
    func pinWidthAndHeight(diameter: CGFloat) {
        
        let dimension = diameter + lineWidth
        heightConstraint?.constant = dimension
        widthConstraint?.constant = dimension
        heightConstraint?.isActive = true
        widthConstraint?.isActive = true
    }
    
    //--------------------------------------------------
    // MARK: - MoleculeViewProtocol
    //--------------------------------------------------
    
    public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
        super.set(with: model, delegateObject, additionalData)
        guard let model = model as? LoadingSpinnerModel else { return }
        
        strokeColor = model.strokeColor.uiColor
        lineWidth = model.lineWidth
        pinWidthAndHeight(diameter: model.diameter)
    }
    
    open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject:  MVMCoreUIDelegateObject?) -> CGFloat? {
        return 40.0
    }
}
