import UIKit

public class TimelineView: UIView {
    public enum Decoration {
        case doubleDot, singleDot, squiggle
    }

    public override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }
    
    open func setup() {
    }

    public var decoration: Decoration = .doubleDot {
        didSet {
            guard oldValue != decoration else {
                return
            }

            switch decoration {
            case .squiggle:
                innerDotShapeLayer.removeFromSuperlayer()
                outerDotShapeLayer.removeFromSuperlayer()
                layer.addSublayer(squiggleShapeLayer)
                updateSquiggleCenterPoint()
            case .doubleDot:
                squiggleShapeLayer.removeFromSuperlayer()
                layer.addSublayer(innerDotShapeLayer)
                layer.addSublayer(outerDotShapeLayer)
            case .singleDot:
                squiggleShapeLayer.removeFromSuperlayer()
                layer.addSublayer(innerDotShapeLayer)
            }

            setNeedsDisplay()
        }
    }
    public var shouldAnimateDots: Bool = false
    public var minimizeUnanimatedDots: Bool = false
    public var timelineColor: UIColor? = nil {
        didSet {
            refreshColors()
        }
    }
    private var color: CGColor {
        return timelineColor?.cgColor ?? tintColor.cgColor
    }

    public var verticalLineWidth: CGFloat = 1.0 {
        didSet {
            squiggleShapeLayer.lineWidth = verticalLineWidth
            setNeedsDisplay()
        }
    }
    
    public var pauseDotsAnimation: Bool = true {
        didSet {
            displayLink?.isPaused = pauseDotsAnimation
        }
    }

    private var dotRadius: CGFloat {
        switch decoration {
        case .singleDot: return 7.0
        default: return 9.0
        }
    }
    private let dotMinRadiusNormal: CGFloat = 0.4

    // At a height of less than 30, (due to rounding) the squiggle's curves don't perfectly align with the straight lines.
    private let squiggleHeight: CGFloat = 30.0
    
    public var dotsY: CGFloat = 0 {
        didSet {
            guard shouldAnimateDots == false || decoration == .squiggle else {
                return
            }

            switch decoration {
            case .doubleDot, .singleDot: updateDotsRadii(to: minimizeUnanimatedDots ? 0.0 : 1.0, at: CGPoint(x: bounds.midX, y: dotsY))
            case .squiggle: updateSquiggleCenterPoint()
            }
            setNeedsDisplay()
        }
    }
    
    override public func tintColorDidChange() {
        super.tintColorDidChange()
        refreshColors()
    }
    
    override public var backgroundColor: UIColor? {
        didSet {
            outerDotShapeLayer.fillColor = backgroundColor?.cgColor
            squiggleShapeLayer.fillColor = backgroundColor?.cgColor
        }
    }

    private lazy var outerDotShapeLayer: CAShapeLayer = {
        let shape = CAShapeLayer()
        shape.fillColor = backgroundColor?.cgColor ?? UIColor.white.cgColor
        shape.strokeColor = color
        shape.lineWidth = 1.0
        if decoration == .doubleDot {
            self.layer.addSublayer(shape)
        }
        return shape
    }()

    private lazy var innerDotShapeLayer: CAShapeLayer = {
        let shape = CAShapeLayer()
        shape.fillColor = color
        shape.strokeColor = color
        shape.lineWidth = 1.0
        if decoration == .doubleDot {
            self.layer.addSublayer(shape)
        }
        return shape
    }()

    private lazy var squiggleShapeLayer: CAShapeLayer = {
        let shape = CAShapeLayer()
        shape.updateSquiggleLocation(height: squiggleHeight, decorationMidY: dotsY, midX: bounds.midX)
        shape.strokeColor = color
        shape.fillColor = backgroundColor?.cgColor ?? UIColor.white.cgColor
        shape.lineWidth = verticalLineWidth
        if decoration == .squiggle {
            self.layer.addSublayer(shape)
        }
        return shape
    }()

    private lazy var displayLink: CADisplayLink? = {
        guard decoration == .doubleDot, shouldAnimateDots == true else {
            return nil
        }
        let link = CADisplayLink(target: self, selector: #selector(maybeUpdateDotsRadii))
        link.add(to: RunLoop.main, forMode: RunLoop.Mode.common)
        return link
    }()
    
    override public func removeFromSuperview() {
        displayLink?.invalidate()
        displayLink = nil
        super.removeFromSuperview()
    }

    override public func draw(_ rect: CGRect) {
        super.draw(rect)
        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }
        drawVerticalLine(in: context, rect: rect)
    }
    
    public var extendTimelineAboveDot: Bool = true {
        didSet {
            if oldValue != extendTimelineAboveDot {
                setNeedsDisplay()
            }
        }
    }
    
    private func drawVerticalLine(in context: CGContext, rect: CGRect) {
        context.setLineWidth(verticalLineWidth)
        context.setStrokeColor(color)
        let lineTopY = extendTimelineAboveDot ? rect.minY : dotsY

        switch decoration {
        case .doubleDot, .singleDot:
            context.move(to: CGPoint(x: rect.midX, y: lineTopY))
            context.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
        case .squiggle:
            if extendTimelineAboveDot {
                context.move(to: CGPoint(x: rect.midX, y: lineTopY))
                context.addLine(to: CGPoint(x: rect.midX, y: dotsY-squiggleHeight/2))
            }
            context.move(to: CGPoint(x: rect.midX, y: dotsY+squiggleHeight/2))
            context.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
        }

        context.strokePath()
    }

    private func refreshColors() {
        outerDotShapeLayer.strokeColor = color
        innerDotShapeLayer.fillColor = color
        innerDotShapeLayer.strokeColor = color
        squiggleShapeLayer.strokeColor = color
        setNeedsDisplay()
    }
    
    // Returns CGFloat in range from 0.0 to 1.0. 0.0 indicates dot should be minimized.
    // 1.0 indicates dot should be maximized. Approaches 1.0 as timelineView.dotY
    // approaches vertical center. Approaches 0.0 as timelineView.dotY approaches top
    // or bottom.
    private func dotRadiusNormal(with y:CGFloat, in container:UIView) -> CGFloat {
        let yInContainer = convert(CGPoint(x:0, y:y), to: container).y
        let halfContainerHeight = container.bounds.size.height * 0.5
        return max(0.0, 1.0 - (abs(yInContainer - halfContainerHeight) / halfContainerHeight))
    }

    private var lastDotRadiusNormal: CGFloat = -1.0 // -1.0 so dots with dotAnimationNormal of "0.0" are visible initially
    
    @objc private func maybeUpdateDotsRadii() {
        guard let containerView = window else {
            return
        }

        // Shift the "full-width dot" point up a bit - otherwise it's in the vertical center of screen.
        let yOffset = containerView.bounds.size.height * 0.15

        var radiusNormal = dotRadiusNormal(with: dotsY + yOffset, in: containerView)

        // Reminder: can reduce precision to 1 (significant digit) to reduce how often dot radii are updated.
        let precision: CGFloat = 2
        let roundingNumber = pow(10, precision)
        radiusNormal = (radiusNormal * roundingNumber).rounded(.up) / roundingNumber
        
        guard radiusNormal != lastDotRadiusNormal else {
            return
        }
        
        updateDotsRadii(to: radiusNormal, at: CGPoint(x: bounds.midX, y: dotsY))
        
        // Progressively fade the inner dot when it gets tiny.
        innerDotShapeLayer.opacity = easeInOutQuart(number: Float(radiusNormal))
        
        lastDotRadiusNormal = radiusNormal
    }
    
    private func updateDotsRadii(to radiusNormal: CGFloat, at center: CGPoint) {
        outerDotShapeLayer.updateDotRadius(dotRadius * max(radiusNormal, dotMinRadiusNormal), center: center)
        innerDotShapeLayer.updateDotRadius(dotRadius * max((radiusNormal - dotMinRadiusNormal), 0.0), center: center)
    }

    private func updateSquiggleCenterPoint() {
        squiggleShapeLayer.updateSquiggleLocation(height: squiggleHeight, decorationMidY: dotsY, midX: bounds.midX)
    }
    
    private func easeInOutQuart(number:Float) -> Float {
        return number < 0.5 ? 8.0 * pow(number, 4) : 1.0 - 8.0 * (number - 1.0) * pow(number, 3)
    }
}

extension CAShapeLayer {
    fileprivate func updateDotRadius(_ radius: CGFloat, center: CGPoint) {
        path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0.0, endAngle:CGFloat.pi * 2.0, clockwise: true).cgPath
    }

    fileprivate func updateSquiggleLocation(height: CGFloat, decorationMidY: CGFloat, midX: CGFloat) {
        let startY = decorationMidY - height/2 // squiggle's middle (not top) should be startY
        let topPoint = CGPoint(x: midX, y: startY)
        let quarterOnePoint = CGPoint(x: midX, y: startY + (height*1/4))
        let midPoint = CGPoint(x: midX, y: startY + (height*2/4))
        let quarterThreePoint = CGPoint(x: midX, y: startY + (height*3/4))
        let bottomPoint = CGPoint(x: midX, y: startY + height)

        /// Math for curves shown/explained on Phab ticket: https://phabricator.wikimedia.org/T258209#6363389
        let eighthOfHeight = height/8
        let circleDiameter = sqrt(2*(eighthOfHeight*eighthOfHeight))
        let radius = circleDiameter/2

        /// Without this adjustment, the `arcCenter`s are not the true center of circle and the squiggle has some jagged edges.
        let centerAdjustedRadius = radius - 1

        let arc1Start = CGPoint(x: midX - radius*3, y: topPoint.y + radius*3)
        let arc1Center = CGPoint(x: arc1Start.x + centerAdjustedRadius, y: arc1Start.y + centerAdjustedRadius)

        let arc2Start = CGPoint(x: midX + radius*1, y: quarterOnePoint.y - radius*1)
        let arc2Center = CGPoint(x: arc2Start.x + centerAdjustedRadius, y: arc2Start.y + centerAdjustedRadius)

        let arc3Start = CGPoint(x: midX - radius*3, y: midPoint.y + radius*3)
        let arc3Center = CGPoint(x: arc3Start.x + centerAdjustedRadius, y: arc3Start.y + centerAdjustedRadius)

        let arc4Start = CGPoint(x: midX + radius*1, y: quarterThreePoint.y - radius*1)
        let arc4Center = CGPoint(x: arc4Start.x + centerAdjustedRadius, y: arc4Start.y + centerAdjustedRadius)

        let squiggle = UIBezierPath()
        let fullCircle = 2 * CGFloat.pi // addArc's angles are in radians, let's make it easier
        squiggle.move(to: topPoint)
        squiggle.addLine(to: arc1Start)
        squiggle.addArc(withCenter: arc1Center, radius: radius, startAngle: fullCircle * 5/8, endAngle: fullCircle * 1/8, clockwise: false)
        squiggle.addLine(to: arc2Start)
        squiggle.addArc(withCenter: arc2Center, radius: radius, startAngle: fullCircle * 5/8, endAngle: fullCircle * 1/8, clockwise: true)
        squiggle.addLine(to: arc3Start)
        squiggle.addArc(withCenter: arc3Center, radius: radius, startAngle: fullCircle * 5/8, endAngle: fullCircle * 1/8, clockwise: false)
        squiggle.addLine(to: arc4Start)
        squiggle.addArc(withCenter: arc4Center, radius: radius, startAngle: fullCircle * 5/8, endAngle: fullCircle * 1/8, clockwise: true)
        squiggle.addLine(to: bottomPoint)

        path = squiggle.cgPath
    }
}
