//
//  CompoundBezierPath.swift
//  lottie-swift
//
//  Created by Brandon Withrow on 1/14/19.
//

import CoreGraphics
import Foundation

/// A collection of BezierPath objects that can be trimmed and added.
///
struct CompoundBezierPath {

  // MARK: Lifecycle

  init() {
    paths = []
    length = 0
  }

  init(path: BezierPath) {
    paths = [path]
    length = path.length
  }

  init(paths: [BezierPath], length: CGFloat) {
    self.paths = paths
    self.length = length
  }

  init(paths: [BezierPath]) {
    self.paths = paths
    var l: CGFloat = 0
    for path in paths {
      l = l + path.length
    }
    length = l
  }

  // MARK: Internal

  let paths: [BezierPath]

  let length: CGFloat

  func addPath(path: BezierPath) -> CompoundBezierPath {
    var newPaths = paths
    newPaths.append(path)
    return CompoundBezierPath(paths: newPaths, length: length + path.length)
  }

  func combine(_ compoundBezier: CompoundBezierPath) -> CompoundBezierPath {
    var newPaths = paths
    newPaths.append(contentsOf: compoundBezier.paths)
    return CompoundBezierPath(paths: newPaths, length: length + compoundBezier.length)
  }

  func trim(fromPosition: CGFloat, toPosition: CGFloat, offset: CGFloat, trimSimultaneously: Bool) -> CompoundBezierPath {
    if fromPosition == toPosition {
      return CompoundBezierPath()
    }

    if trimSimultaneously {
      /// Trim each path individually.
      var newPaths = [BezierPath]()
      for path in paths {
        newPaths.append(contentsOf: path.trim(
          fromLength: fromPosition * path.length,
          toLength: toPosition * path.length,
          offsetLength: offset * path.length))
      }
      return CompoundBezierPath(paths: newPaths)
    }

    /// Normalize lengths to the curve length.
    var startPosition = (fromPosition + offset).truncatingRemainder(dividingBy: 1)
    var endPosition = (toPosition + offset).truncatingRemainder(dividingBy: 1)

    if startPosition < 0 {
      startPosition = 1 + startPosition
    }

    if endPosition < 0 {
      endPosition = 1 + endPosition
    }

    if startPosition == 1 {
      startPosition = 0
    }
    if endPosition == 0 {
      endPosition = 1
    }

    if
      startPosition == 0 && endPosition == 1 ||
      startPosition == endPosition ||
      startPosition == 1 && endPosition == 0
    {
      /// The trim encompasses the entire path. Return.
      return self
    }

    var positions: [(start: CGFloat, end: CGFloat)]
    if endPosition < startPosition {
      positions = [
        (start: 0, end: endPosition * length),
        (start: startPosition * length, end: length),
      ]
    } else {
      positions = [(start: startPosition * length, end: endPosition * length)]
    }

    var compoundPath = CompoundBezierPath()
    var trim = positions.remove(at: 0)
    var pathStartPosition: CGFloat = 0

    var finishedTrimming = false
    var i = 0

    while !finishedTrimming {
      if paths.count <= i {
        /// Rounding errors
        finishedTrimming = true
        continue
      }
      let path = paths[i]

      let pathEndPosition = pathStartPosition + path.length

      if pathEndPosition < trim.start {
        /// Path is not included in the trim, continue.
        pathStartPosition = pathEndPosition
        i = i + 1
        continue

      } else if trim.start <= pathStartPosition, pathEndPosition <= trim.end {
        /// Full Path is inside of trim. Add full path.
        compoundPath = compoundPath.addPath(path: path)
      } else {
        if
          let trimPath = path.trim(
            fromLength: trim.start > pathStartPosition ? (trim.start - pathStartPosition) : 0,
            toLength: trim.end < pathEndPosition ? (trim.end - pathStartPosition) : path.length,
            offsetLength: 0).first
        {
          compoundPath = compoundPath.addPath(path: trimPath)
        }
      }

      if trim.end <= pathEndPosition {
        /// We are done with the current trim.
        /// Advance trim but remain on the same path in case the next trim overlaps it.
        if positions.count > 0 {
          trim = positions.remove(at: 0)
        } else {
          finishedTrimming = true
        }
      } else {
        pathStartPosition = pathEndPosition
        i = i + 1
      }
    }
    return compoundPath
  }

}
