//
//  Shape.swift
//  lottie-swift
//
//  Created by Brandon Withrow on 1/8/19.
//

import CoreGraphics
import Foundation

// MARK: - BezierPath

/// A container that holds instructions for creating a single, unbroken Bezier Path.
struct BezierPath {

  // MARK: Lifecycle

  /// Initializes a new Bezier Path.
  init(startPoint: CurveVertex) {
    elements = [PathElement(vertex: startPoint)]
    length = 0
    closed = false
  }

  init() {
    elements = []
    length = 0
    closed = false
  }

  // MARK: Internal

  /// The elements of the path
  fileprivate(set) var elements: [PathElement]

  /// If the path is closed or not.
  fileprivate(set) var closed: Bool

  /// The total length of the path.
  fileprivate(set) var length: CGFloat

  mutating func moveToStartPoint(_ vertex: CurveVertex) {
    elements = [PathElement(vertex: vertex)]
    length = 0
  }

  mutating func addVertex(_ vertex: CurveVertex) {
    guard let previous = elements.last else {
      addElement(PathElement(vertex: vertex))
      return
    }
    addElement(previous.pathElementTo(vertex))
  }

  mutating func addCurve(toPoint: CGPoint, outTangent: CGPoint, inTangent: CGPoint) {
    guard let previous = elements.last else { return }
    let newVertex = CurveVertex(inTangent, toPoint, toPoint)
    updateVertex(
      CurveVertex(previous.vertex.inTangent, previous.vertex.point, outTangent),
      atIndex: elements.endIndex - 1,
      remeasure: false)
    addVertex(newVertex)
  }

  mutating func addLine(toPoint: CGPoint) {
    guard let previous = elements.last else { return }
    let newVertex = CurveVertex(point: toPoint, inTangentRelative: .zero, outTangentRelative: .zero)
    updateVertex(
      CurveVertex(previous.vertex.inTangent, previous.vertex.point, previous.vertex.point),
      atIndex: elements.endIndex - 1,
      remeasure: false)
    addVertex(newVertex)
  }

  mutating func close() {
    closed = true
  }

  mutating func addElement(_ pathElement: PathElement) {
    elements.append(pathElement)
    length = length + pathElement.length
  }

  mutating func updateVertex(_ vertex: CurveVertex, atIndex: Int, remeasure: Bool) {
    if remeasure {
      var newElement: PathElement
      if atIndex > 0 {
        let previousElement = elements[atIndex - 1]
        newElement = previousElement.pathElementTo(vertex)
      } else {
        newElement = PathElement(vertex: vertex)
      }
      elements[atIndex] = newElement

      if atIndex + 1 < elements.count{
        let nextElement = elements[atIndex + 1]
        elements[atIndex + 1] = newElement.pathElementTo(nextElement.vertex)
      }

    } else {
      let oldElement = elements[atIndex]
      elements[atIndex] = oldElement.updateVertex(newVertex: vertex)
    }
  }

  /**
   Trims a path fromLength toLength with an offset.

   Length and offset are defined in the length coordinate space.
   If any argument is outside the range of this path, then it will be looped over the path from finish to start.

   Cutting the curve when fromLength is less than toLength
   x                    x                                 x                          x
   ~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooooooooooooooooooooooo-------------------
   |Offset        |fromLength                             toLength|                  |

   Cutting the curve when from Length is greater than toLength
   x                x                    x               x                           x
   oooooooooooooooooo--------------------~~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooo
   |        toLength|                    |Offset         |fromLength                 |

   */
  func trim(fromLength: CGFloat, toLength: CGFloat, offsetLength: CGFloat) -> [BezierPath] {
    guard elements.count > 1 else {
      return []
    }

    if fromLength == toLength {
      return []
    }

    /// Normalize lengths to the curve length.
    var start = (fromLength + offsetLength).truncatingRemainder(dividingBy: length)
    var end = (toLength + offsetLength).truncatingRemainder(dividingBy: length)

    if start < 0 {
      start = length + start
    }

    if end < 0 {
      end = length + end
    }

    if start == length {
      start = 0
    }
    if end == 0 {
      end = length
    }

    if
      start == 0 && end == length ||
      start == end ||
      start == length && end == 0
    {
      /// The trim encompasses the entire path. Return.
      return [self]
    }

    if start > end {
      // Start is greater than end. Two paths are returned.
      return trimPathAtLengths(positions: [(start: 0, end: end), (start: start, end: length)])
    }

    return trimPathAtLengths(positions: [(start: start, end: end)])
  }

  // MARK: Fileprivate

  fileprivate func trimPathAtLengths(positions: [(start: CGFloat, end: CGFloat)]) -> [BezierPath] {
    guard positions.count > 0 else {
      return []
    }
    var remainingPositions = positions

    var trim = remainingPositions.remove(at: 0)

    var paths = [BezierPath]()

    var runningLength: CGFloat = 0
    var finishedTrimming: Bool = false
    var pathElements = elements

    var currentPath = BezierPath()
    var i: Int = 0

    while !finishedTrimming {
      if pathElements.count <= i {
        /// Do this for rounding errors
        paths.append(currentPath)
        finishedTrimming = true
        continue
      }
      /// Loop through and add elements within start->end range.
      /// Get current element
      let element = pathElements[i]

      /// Calculate new running length.
      let newLength = runningLength + element.length

      if newLength < trim.start {
        /// Element is not included in the trim, continue.
        runningLength = newLength
        i = i + 1
        /// Increment index, we are done with this element.
        continue
      }

      if newLength == trim.start {
        /// Current element IS the start element.
        /// For start we want to add a zero length element.
        currentPath.moveToStartPoint(element.vertex)
        runningLength = newLength
        i = i + 1
        /// Increment index, we are done with this element.
        continue
      }

      if runningLength < trim.start, trim.start < newLength, currentPath.elements.count == 0 {
        /// The start of the trim is between this element and the previous, trim.
        /// Get previous element.
        let previousElement = pathElements[i - 1]
        /// Trim it
        let trimLength = trim.start - runningLength
        let trimResults = element.splitElementAtPosition(fromElement: previousElement, atLength: trimLength)
        /// Add the right span start.
        currentPath.moveToStartPoint(trimResults.rightSpan.start.vertex)

        pathElements[i] = trimResults.rightSpan.end
        pathElements[i - 1] = trimResults.rightSpan.start
        runningLength = runningLength + trimResults.leftSpan.end.length
        /// Dont increment index or the current length, the end of this path can be within this span.
        continue
      }

      if trim.start < newLength, newLength < trim.end {
        /// Element lies within the trim span.
        currentPath.addElement(element)
        runningLength = newLength
        i = i + 1
        continue
      }

      if newLength == trim.end {
        /// Element is the end element.
        /// The element could have a new length if it's added right after the start node.
        currentPath.addElement(element)
        /// We are done with this span.
        runningLength = newLength
        i = i + 1
        /// Allow the path to be finalized.
        /// Fall through to finalize path and move to next position
      }

      if runningLength < trim.end, trim.end < newLength {
        /// New element must be cut for end.
        /// Get previous element.
        let previousElement = pathElements[i - 1]
        /// Trim it
        let trimLength = trim.end - runningLength
        let trimResults = element.splitElementAtPosition(fromElement: previousElement, atLength: trimLength)
        /// Add the left span end.

        currentPath.updateVertex(trimResults.leftSpan.start.vertex, atIndex: currentPath.elements.count - 1, remeasure: false)
        currentPath.addElement(trimResults.leftSpan.end)

        pathElements[i] = trimResults.rightSpan.end
        pathElements[i - 1] = trimResults.rightSpan.start
        runningLength = runningLength + trimResults.leftSpan.end.length
        /// Dont increment index or the current length, the start of the next path can be within this span.
        /// We are done with this span.
        /// Allow the path to be finalized.
        /// Fall through to finalize path and move to next position
      }

      paths.append(currentPath)
      currentPath = BezierPath()
      if remainingPositions.count > 0 {
        trim = remainingPositions.remove(at: 0)
      } else {
        finishedTrimming = true
      }
    }
    return paths
  }

}

// MARK: Codable

extension BezierPath: Codable {

  // MARK: Lifecycle

  init(from decoder: Decoder) throws {
    let container: KeyedDecodingContainer<BezierPath.CodingKeys>

    if let keyedContainer = try? decoder.container(keyedBy: BezierPath.CodingKeys.self) {
      container = keyedContainer
    } else {
      var unkeyedContainer = try decoder.unkeyedContainer()
      container = try unkeyedContainer.nestedContainer(keyedBy: BezierPath.CodingKeys.self)
    }

    closed = try container.decodeIfPresent(Bool.self, forKey: .closed) ?? true

    var vertexContainer = try container.nestedUnkeyedContainer(forKey: .vertices)
    var inPointsContainer = try container.nestedUnkeyedContainer(forKey: .inPoints)
    var outPointsContainer = try container.nestedUnkeyedContainer(forKey: .outPoints)

    guard vertexContainer.count == inPointsContainer.count, inPointsContainer.count == outPointsContainer.count else {
      /// Will throw an error if vertex, inpoints, and outpoints are not the same length.
      /// This error is to be expected.
      throw DecodingError.dataCorruptedError(
        forKey: CodingKeys.vertices,
        in: container,
        debugDescription: "Vertex data does not match In Tangents and Out Tangents")
    }

    guard let count = vertexContainer.count, count > 0 else {
      length = 0
      elements = []
      return
    }

    var decodedElements = [PathElement]()

    /// Create first point
    let firstVertex = CurveVertex(
      point: try vertexContainer.decode(CGPoint.self),
      inTangentRelative: try inPointsContainer.decode(CGPoint.self),
      outTangentRelative: try outPointsContainer.decode(CGPoint.self))
    var previousElement = PathElement(vertex: firstVertex)
    decodedElements.append(previousElement)

    var totalLength: CGFloat = 0
    while !vertexContainer.isAtEnd {
      /// Get the next vertex data.
      let vertex = CurveVertex(
        point: try vertexContainer.decode(CGPoint.self),
        inTangentRelative: try inPointsContainer.decode(CGPoint.self),
        outTangentRelative: try outPointsContainer.decode(CGPoint.self))
      let pathElement = previousElement.pathElementTo(vertex)
      decodedElements.append(pathElement)
      previousElement = pathElement
      totalLength = totalLength + pathElement.length
    }
    if closed {
      let closeElement = previousElement.pathElementTo(firstVertex)
      decodedElements.append(closeElement)
      totalLength = totalLength + closeElement.length
    }
    length = totalLength
    elements = decodedElements
  }

  // MARK: Internal

  /**
   The BezierPath container is encoded and decoded from the JSON format
   that defines points for a lottie animation.

   {
   "c" = Bool
   "i" = [[Double]],
   "o" = [[Double]],
   "v" = [[Double]]
   }

   */

  enum CodingKeys: String, CodingKey {
    case closed = "c"
    case inPoints = "i"
    case outPoints = "o"
    case vertices = "v"
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: BezierPath.CodingKeys.self)
    try container.encode(closed, forKey: .closed)

    var vertexContainer = container.nestedUnkeyedContainer(forKey: .vertices)
    var inPointsContainer = container.nestedUnkeyedContainer(forKey: .inPoints)
    var outPointsContainer = container.nestedUnkeyedContainer(forKey: .outPoints)

    /// If closed path, ignore the final element.
    let finalIndex = closed ? elements.endIndex - 1 : elements.endIndex
    for i in 0..<finalIndex {
      let element = elements[i]
      try vertexContainer.encode(element.vertex.point)
      try inPointsContainer.encode(element.vertex.inTangentRelative)
      try outPointsContainer.encode(element.vertex.outTangentRelative)
    }

  }
}

extension BezierPath {

  func cgPath() -> CGPath {
    let cgPath = CGMutablePath()

    var previousElement: PathElement?
    for element in elements {
      if let previous = previousElement {
        if previous.vertex.outTangentRelative.isZero && element.vertex.inTangentRelative.isZero {
          cgPath.addLine(to: element.vertex.point)
        } else {
          cgPath.addCurve(to: element.vertex.point, control1: previous.vertex.outTangent, control2: element.vertex.inTangent)
        }
      } else {
        cgPath.move(to: element.vertex.point)
      }
      previousElement = element
    }
    if closed {
      cgPath.closeSubpath()
    }
    return cgPath
  }

}
