//
//  GraphLine.swift
//  1Weather
//
//  Created by Dmitry Stepanets on 26.02.2021.
//

import UIKit
import BezierKit

struct LineDot {
    let center:CGPoint
    let shape:CAShapeLayer
}

struct GraphLine {
    //Private
    private let kInset:CGFloat = 10
    private let kIntersectAccuracy:CGFloat = 0
    private var points = [CGPoint]()
    private let settings:GraphLineSettings
    private let onGetGraphRect: GraphRectClosure
    private var sections = [CubicCurve]()
    
    //Public
    public typealias GraphRectClosure = () -> CGRect
    public private(set) var lineShape = CAShapeLayer()
    public private(set) var tintLineShape = CAShapeLayer()
    public private(set) var lineDots = [LineDot]()
    
    init(settings:GraphLineSettings, onGetGraphRect:@escaping GraphRectClosure) {
        self.onGetGraphRect = onGetGraphRect
        self.settings = settings
        
        //Line
        lineShape.fillColor = UIColor.clear.cgColor
        lineShape.strokeColor = settings.color.cgColor
        lineShape.lineWidth = settings.lineWidth
        lineShape.lineCap = .round
        lineShape.lineJoin = .round
        
        //Shadow
        lineShape.shadowColor = UIColor.black.cgColor
        lineShape.shadowOffset = .init(width: 0, height: 6)
        lineShape.shadowRadius = 3
        lineShape.shadowOpacity = 0.1
        
        //Tint line
        tintLineShape.fillColor = UIColor.clear.cgColor
        tintLineShape.strokeColor = settings.tintColor.cgColor
        tintLineShape.lineWidth = settings.lineWidth
        tintLineShape.lineCap = .round
        tintLineShape.lineJoin = .round
        tintLineShape.path = nil
    }
    
    private mutating func linePath(from points:[CGPoint]) -> UIBezierPath {
        let path = UIBezierPath()
        let startPoint = CGPoint(x: 0, y: points.first?.y ?? 0)
        let endPoint = CGPoint(x: self.onGetGraphRect().width, y: points.last?.y ?? 0)
        
        var pointsToAdd = [CGPoint]()
        pointsToAdd.append(startPoint)
        pointsToAdd.append(contentsOf: points)
        pointsToAdd.append(endPoint)
        
        path.move(to: pointsToAdd.first!)
        if pointsToAdd.count == 2 {
            path.addLine(to: points[1])
            return path
        }
        
        let cubicCurveAlgorithm = CubicCurveAlgorithm()
        let controlPoints = cubicCurveAlgorithm.controlPointsFromPoints(dataPoints: pointsToAdd)
        for index in 1..<pointsToAdd.count {
            sections.append(.init(p0: pointsToAdd[index - 1],
                                p1: controlPoints[index - 1].controlPoint1,
                                p2: controlPoints[index - 1].controlPoint2,
                                p3: pointsToAdd[index]))

            path.addCurve(to: pointsToAdd[index],
                          controlPoint1: controlPoints[index - 1].controlPoint1,
                          controlPoint2: controlPoints[index - 1].controlPoint2)
        }
        
        return path
    }
    
    //Public
    func tintLineFrom(startPointX:CGFloat, to endPointX:CGFloat) {
        func getSubcurvePath(baseCurve:CubicCurve, leftBoundary:LineSegment, rightBoundary:LineSegment) -> UIBezierPath? {
            guard
                let leftIntersection = baseCurve.intersections(with: leftBoundary).first,
                let rightIntersection = baseCurve.intersections(with: rightBoundary).first
            else {
                return nil
            }
            
            let subcurve = baseCurve.split(from: leftIntersection.t1, to: rightIntersection.t1)
            let path = UIBezierPath()
            path.move(to: subcurve.startingPoint)
            path.addCurve(to: subcurve.endingPoint, controlPoint1: subcurve.p1, controlPoint2: subcurve.p2)
            
            return path
        }
        
        let leftLine = LineSegment(p0: .init(x: startPointX + kIntersectAccuracy, y: 0),
                                   p1: .init(x: startPointX + kIntersectAccuracy, y: self.onGetGraphRect().height))
        let rightLine = LineSegment(p0: .init(x: endPointX - kIntersectAccuracy, y: 0),
                                    p1: .init(x: endPointX - kIntersectAccuracy, y: self.onGetGraphRect().height))
        
        //Get all sections for the given tint and cut them
        let tintPath = UIBezierPath()
        for section in sections {
            if section.startingPoint.x >= leftLine.p0.x || section.endingPoint.x <= rightLine.p0.x {
                let maxLeftPointX = max(section.startingPoint.x, leftLine.startingPoint.x)
                let minRightPointX = min(section.endingPoint.x, rightLine.endingPoint.x)
                
                let leftBoundary = LineSegment(p0: .init(x: maxLeftPointX, y: -kInset),
                                               p1: .init(x: maxLeftPointX, y: self.onGetGraphRect().height + kInset * 2))
                let rightBoundary = LineSegment(p0: .init(x: minRightPointX, y: -kInset),
                                                p1: .init(x: minRightPointX, y: self.onGetGraphRect().height + kInset * 2))
                if let subcurve = getSubcurvePath(baseCurve: section,
                                                  leftBoundary: leftBoundary,
                                                  rightBoundary: rightBoundary)
                {
                    tintPath.append(subcurve)
                }
            }
        }
        
        //Check for empty path
        if tintPath.isEmpty { return }
        
        tintLineShape.path = tintPath.cgPath
    }
    
    func tintDotAt(point:CGPoint) {
        lineDots.forEach {
            $0.shape.strokeColor = settings.dotColor.cgColor
        }
        
        guard let dotToTint = (lineDots.first{$0.center == point}) else { return }
        dotToTint.shape.strokeColor = settings.tintColor.cgColor
    }
    
    mutating func updateWith(points:[CGPoint]) {
        self.lineDots.forEach {
            $0.shape.removeFromSuperlayer()
        }
        
        self.sections.removeAll()
        self.lineDots.removeAll()
        self.points = points
        self.tintLineShape.path = nil
        
        guard !self.points.isEmpty else {
            lineShape.path = nil
            return
        }
        
        //Add new dots
        self.points.forEach {
            let dotShape = CAShapeLayer()
            dotShape.path = UIBezierPath(arcCenter: $0,
                                         radius: settings.dotRadius,
                                            startAngle: 0,
                                            endAngle: .pi * 2,
                                            clockwise: true).cgPath
            dotShape.fillColor = UIColor.white.cgColor
            dotShape.strokeColor = settings.dotColor.cgColor
            dotShape.lineWidth = settings.dotLineWidth
            lineDots.append(.init(center: $0, shape: dotShape))
        }
        
        //Refresh line path
        lineShape.path = self.linePath(from: points).cgPath
    }
}
