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

import CoreGraphics

// 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
  private(set) var elements: [PathElement]

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

  /// The total length of the path.
  private(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: Private

  private 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 = false
    var pathElements = elements

    var currentPath = BezierPath()
    var i = 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)
    }
  }
}

// MARK: AnyInitializable

extension BezierPath: AnyInitializable {

  init(value: Any) throws {
    var pathDictionary: [String: Any]
    if
      let array = value as? [[String: Any]],
      let firstDictionary = array.first
    {
      pathDictionary = firstDictionary
    } else if let dictionary = value as? [String: Any] {
      pathDictionary = dictionary
    } else {
      throw InitializableError.invalidInput()
    }
    closed = (try? pathDictionary.value(for: CodingKeys.closed)) ?? true
    var vertexDictionaries: [Any] = try pathDictionary.value(for: CodingKeys.vertices)
    var inPointsDictionaries: [Any] = try pathDictionary.value(for: CodingKeys.inPoints)
    var outPointsDictionaries: [Any] = try pathDictionary.value(for: CodingKeys.outPoints)
    guard
      vertexDictionaries.count == inPointsDictionaries.count,
      inPointsDictionaries.count == outPointsDictionaries.count
    else {
      throw InitializableError.invalidInput()
    }
    guard vertexDictionaries.count > 0 else {
      length = 0
      elements = []
      return
    }

    var decodedElements = [PathElement]()
    let firstVertexDictionary = vertexDictionaries.removeFirst()
    let firstInPointsDictionary = inPointsDictionaries.removeFirst()
    let firstOutPointsDictionary = outPointsDictionaries.removeFirst()
    let firstVertex = CurveVertex(
      point: try CGPoint(value: firstVertexDictionary),
      inTangentRelative: try CGPoint(value: firstInPointsDictionary),
      outTangentRelative: try CGPoint(value: firstOutPointsDictionary))
    var previousElement = PathElement(vertex: firstVertex)
    decodedElements.append(previousElement)

    var totalLength: CGFloat = 0
    while vertexDictionaries.count > 0 {
      let vertexDictionary = vertexDictionaries.removeFirst()
      let inPointsDictionary = inPointsDictionaries.removeFirst()
      let outPointsDictionary = outPointsDictionaries.removeFirst()
      let vertex = CurveVertex(
        point: try CGPoint(value: vertexDictionary),
        inTangentRelative: try CGPoint(value: inPointsDictionary),
        outTangentRelative: try CGPoint(value: outPointsDictionary))
      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
  }

}

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
  }

}
