import { Point } from './point'
import { Line } from './line'
import { Rectangle } from './rectangle'
import { Polyline } from './polyline'
import { Geometry } from './geometry'

export class Curve extends Geometry {
  start: Point
  end: Point
  controlPoint1: Point
  controlPoint2: Point

  PRECISION = 3

  protected get [Symbol.toStringTag]() {
    return Curve.toStringTag
  }

  constructor(
    start: Point.PointLike | Point.PointData,
    controlPoint1: Point.PointLike | Point.PointData,
    controlPoint2: Point.PointLike | Point.PointData,
    end: Point.PointLike | Point.PointData,
  ) {
    super()
    this.start = Point.create(start)
    this.controlPoint1 = Point.create(controlPoint1)
    this.controlPoint2 = Point.create(controlPoint2)
    this.end = Point.create(end)
  }

  bbox() {
    const start = this.start
    const controlPoint1 = this.controlPoint1
    const controlPoint2 = this.controlPoint2
    const end = this.end

    const x0 = start.x
    const y0 = start.y
    const x1 = controlPoint1.x
    const y1 = controlPoint1.y
    const x2 = controlPoint2.x
    const y2 = controlPoint2.y
    const x3 = end.x
    const y3 = end.y

    const points = [] // local extremes
    const tvalues = [] // t values of local extremes
    const bounds: [number[], number[]] = [[], []]

    let a
    let b
    let c
    let t
    let t1
    let t2
    let b2ac
    let sqrtb2ac

    for (let i = 0; i < 2; i += 1) {
      if (i === 0) {
        b = 6 * x0 - 12 * x1 + 6 * x2
        a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3
        c = 3 * x1 - 3 * x0
      } else {
        b = 6 * y0 - 12 * y1 + 6 * y2
        a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3
        c = 3 * y1 - 3 * y0
      }

      if (Math.abs(a) < 1e-12) {
        if (Math.abs(b) < 1e-12) {
          continue
        }

        t = -c / b
        if (t > 0 && t < 1) tvalues.push(t)

        continue
      }

      b2ac = b * b - 4 * c * a
      sqrtb2ac = Math.sqrt(b2ac)

      if (b2ac < 0) continue

      t1 = (-b + sqrtb2ac) / (2 * a)
      if (t1 > 0 && t1 < 1) tvalues.push(t1)

      t2 = (-b - sqrtb2ac) / (2 * a)
      if (t2 > 0 && t2 < 1) tvalues.push(t2)
    }

    let x
    let y
    let mt
    let j = tvalues.length
    const jlen = j

    while (j) {
      j -= 1
      t = tvalues[j]
      mt = 1 - t

      x =
        mt * mt * mt * x0 +
        3 * mt * mt * t * x1 +
        3 * mt * t * t * x2 +
        t * t * t * x3
      bounds[0][j] = x

      y =
        mt * mt * mt * y0 +
        3 * mt * mt * t * y1 +
        3 * mt * t * t * y2 +
        t * t * t * y3

      bounds[1][j] = y
      points[j] = { X: x, Y: y }
    }

    tvalues[jlen] = 0
    tvalues[jlen + 1] = 1

    points[jlen] = { X: x0, Y: y0 }
    points[jlen + 1] = { X: x3, Y: y3 }

    bounds[0][jlen] = x0
    bounds[1][jlen] = y0

    bounds[0][jlen + 1] = x3
    bounds[1][jlen + 1] = y3

    tvalues.length = jlen + 2
    bounds[0].length = jlen + 2
    bounds[1].length = jlen + 2
    points.length = jlen + 2

    const left = Math.min.apply(null, bounds[0])
    const top = Math.min.apply(null, bounds[1])
    const right = Math.max.apply(null, bounds[0])
    const bottom = Math.max.apply(null, bounds[1])

    return new Rectangle(left, top, right - left, bottom - top)
  }

  closestPoint(
    p: Point.PointLike | Point.PointData,
    options: Curve.Options = {},
  ) {
    return this.pointAtT(this.closestPointT(p, options))
  }

  closestPointLength(
    p: Point.PointLike | Point.PointData,
    options: Curve.Options = {},
  ) {
    const opts = this.getOptions(options)
    return this.lengthAtT(this.closestPointT(p, opts), opts)
  }

  closestPointNormalizedLength(
    p: Point.PointLike | Point.PointData,
    options: Curve.Options = {},
  ) {
    const opts = this.getOptions(options)
    const cpLength = this.closestPointLength(p, opts)
    if (!cpLength) {
      return 0
    }

    const length = this.length(opts)
    if (length === 0) {
      return 0
    }

    return cpLength / length
  }

  closestPointT(
    p: Point.PointLike | Point.PointData,
    options: Curve.Options = {},
  ) {
    const precision = this.getPrecision(options)
    const subdivisions = this.getDivisions(options)
    const precisionRatio = Math.pow(10, -precision) // eslint-disable-line

    let investigatedSubdivision: Curve | null = null
    let investigatedSubdivisionStartT = 0
    let investigatedSubdivisionEndT = 0
    let distFromStart = 0
    let distFromEnd = 0
    let chordLength = 0
    let minSumDist: number | null = null

    const count = subdivisions.length
    let piece = count > 0 ? 1 / count : 0

    subdivisions.forEach((division, i) => {
      const startDist = division.start.distance(p)
      const endDist = division.end.distance(p)
      const sumDist = startDist + endDist
      if (minSumDist == null || sumDist < minSumDist) {
        investigatedSubdivision = division
        investigatedSubdivisionStartT = i * piece
        investigatedSubdivisionEndT = (i + 1) * piece

        distFromStart = startDist
        distFromEnd = endDist
        minSumDist = sumDist
        chordLength = division.endpointDistance()
      }
    })

    // Recursively divide investigated subdivision, until distance between
    // baselinePoint and closest path endpoint is within `10^(-precision)`,
    // then return the closest endpoint of that final subdivision.
    // eslint-disable-next-line
    while (true) {
      // check if we have reached at least one required observed precision
      // - calculated as: the difference in distances from point to start and end divided by the distance
      // - note that this function is not monotonic = it doesn't converge stably but has "teeth"
      // - the function decreases while one of the endpoints is fixed but "jumps" whenever we switch
      // - this criterion works well for points lying far away from the curve
      const startPrecisionRatio = distFromStart
        ? Math.abs(distFromStart - distFromEnd!) / distFromStart
        : 0

      const endPrecisionRatio =
        distFromEnd != null
          ? Math.abs(distFromStart! - distFromEnd) / distFromEnd
          : 0

      const hasRequiredPrecision =
        startPrecisionRatio < precisionRatio ||
        endPrecisionRatio < precisionRatio

      // check if we have reached at least one required minimal distance
      // - calculated as: the subdivision chord length multiplied by precisionRatio
      // - calculation is relative so it will work for arbitrarily large/small curves and their subdivisions
      // - this is a backup criterion that works well for points lying "almost at" the curve
      const hasMiniStartDistance = distFromStart
        ? distFromStart < chordLength * precisionRatio
        : true
      const hasMiniEndDistance = distFromEnd
        ? distFromEnd < chordLength * precisionRatio
        : true
      const hasMiniDistance = hasMiniStartDistance || hasMiniEndDistance

      if (hasRequiredPrecision || hasMiniDistance) {
        return distFromStart <= distFromEnd
          ? investigatedSubdivisionStartT
          : investigatedSubdivisionEndT
      }

      // otherwise, set up for next iteration
      const divided: [Curve, Curve] = investigatedSubdivision!.divide(0.5)
      piece /= 2

      const startDist1 = divided[0].start.distance(p)
      const endDist1 = divided[0].end.distance(p)
      const sumDist1 = startDist1 + endDist1

      const startDist2 = divided[1].start.distance(p)
      const endDist2 = divided[1].end.distance(p)
      const sumDist2 = startDist2 + endDist2

      if (sumDist1 <= sumDist2) {
        investigatedSubdivision = divided[0]
        investigatedSubdivisionEndT -= piece
        distFromStart = startDist1
        distFromEnd = endDist1
      } else {
        investigatedSubdivision = divided[1]
        investigatedSubdivisionStartT += piece
        distFromStart = startDist2
        distFromEnd = endDist2
      }
    }
  }

  closestPointTangent(
    p: Point.PointLike | Point.PointData,
    options: Curve.Options = {},
  ) {
    return this.tangentAtT(this.closestPointT(p, options))
  }

  containsPoint(
    p: Point.PointLike | Point.PointData,
    options: Curve.Options = {},
  ) {
    const polyline = this.toPolyline(options)
    return polyline.containsPoint(p)
  }

  divideAt(ratio: number, options: Curve.Options = {}): [Curve, Curve] {
    if (ratio <= 0) {
      return this.divideAtT(0)
    }

    if (ratio >= 1) {
      return this.divideAtT(1)
    }

    const t = this.tAt(ratio, options)
    return this.divideAtT(t)
  }

  divideAtLength(length: number, options: Curve.Options = {}): [Curve, Curve] {
    const t = this.tAtLength(length, options)
    return this.divideAtT(t)
  }

  divide(t: number) {
    return this.divideAtT(t)
  }

  divideAtT(t: number): [Curve, Curve] {
    const start = this.start
    const controlPoint1 = this.controlPoint1
    const controlPoint2 = this.controlPoint2
    const end = this.end

    if (t <= 0) {
      return [
        new Curve(start, start, start, start),
        new Curve(start, controlPoint1, controlPoint2, end),
      ]
    }

    if (t >= 1) {
      return [
        new Curve(start, controlPoint1, controlPoint2, end),
        new Curve(end, end, end, end),
      ]
    }

    const dividerPoints = this.getSkeletonPoints(t)
    const startControl1 = dividerPoints.startControlPoint1
    const startControl2 = dividerPoints.startControlPoint2
    const divider = dividerPoints.divider
    const dividerControl1 = dividerPoints.dividerControlPoint1
    const dividerControl2 = dividerPoints.dividerControlPoint2

    return [
      new Curve(start, startControl1, startControl2, divider),
      new Curve(divider, dividerControl1, dividerControl2, end),
    ]
  }

  endpointDistance() {
    return this.start.distance(this.end)
  }

  getSkeletonPoints(t: number) {
    const start = this.start
    const control1 = this.controlPoint1
    const control2 = this.controlPoint2
    const end = this.end

    // shortcuts for `t` values that are out of range
    if (t <= 0) {
      return {
        startControlPoint1: start.clone(),
        startControlPoint2: start.clone(),
        divider: start.clone(),
        dividerControlPoint1: control1.clone(),
        dividerControlPoint2: control2.clone(),
      }
    }

    if (t >= 1) {
      return {
        startControlPoint1: control1.clone(),
        startControlPoint2: control2.clone(),
        divider: end.clone(),
        dividerControlPoint1: end.clone(),
        dividerControlPoint2: end.clone(),
      }
    }

    const midpoint1 = new Line(start, control1).pointAt(t)
    const midpoint2 = new Line(control1, control2).pointAt(t)
    const midpoint3 = new Line(control2, end).pointAt(t)

    const subControl1 = new Line(midpoint1, midpoint2).pointAt(t)
    const subControl2 = new Line(midpoint2, midpoint3).pointAt(t)

    const divideLine = new Line(subControl1, subControl2).pointAt(t)

    return {
      startControlPoint1: midpoint1,
      startControlPoint2: subControl1,
      divider: divideLine,
      dividerControlPoint1: subControl2,
      dividerControlPoint2: midpoint3,
    }
  }

  getSubdivisions(options: Curve.Options = {}): Curve[] {
    const precision = this.getPrecision(options)
    let subdivisions = [
      new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end),
    ]

    if (precision === 0) {
      return subdivisions
    }

    let previousLength = this.endpointDistance()
    const precisionRatio = Math.pow(10, -precision) // eslint-disable-line

    // Recursively divide curve at `t = 0.5`, until the difference between
    // observed length at subsequent iterations is lower than precision.
    let iteration = 0
    // eslint-disable-next-line
    while (true) {
      iteration += 1

      const divisions: Curve[] = []
      subdivisions.forEach((c) => {
        // dividing at t = 0.5 (not at middle length!)
        const divided = c.divide(0.5)
        divisions.push(divided[0], divided[1])
      })

      // measure new length
      const length = divisions.reduce(
        (memo, c) => memo + c.endpointDistance(),
        0,
      )

      // check if we have reached required observed precision
      // sine-like curves may have the same observed length in iteration 0 and 1 - skip iteration 1
      // not a problem for further iterations because cubic curves cannot have more than two local extrema
      // (i.e. cubic curves cannot intersect the baseline more than once)
      // therefore two subsequent iterations cannot produce sampling with equal length
      const ratio = length !== 0 ? (length - previousLength) / length : 0
      if (iteration > 1 && ratio < precisionRatio) {
        return divisions
      }

      subdivisions = divisions
      previousLength = length
    }
  }

  length(options: Curve.Options = {}) {
    const divisions = this.getDivisions(options)
    return divisions.reduce((memo, c) => {
      return memo + c.endpointDistance()
    }, 0)
  }

  lengthAtT(t: number, options: Curve.Options = {}) {
    if (t <= 0) {
      return 0
    }

    const precision =
      options.precision === undefined ? this.PRECISION : options.precision
    const subCurve = this.divide(t)[0]
    return subCurve.length({ precision })
  }

  pointAt(ratio: number, options: Curve.Options = {}) {
    if (ratio <= 0) {
      return this.start.clone()
    }

    if (ratio >= 1) {
      return this.end.clone()
    }

    const t = this.tAt(ratio, options)
    return this.pointAtT(t)
  }

  pointAtLength(length: number, options: Curve.Options = {}) {
    const t = this.tAtLength(length, options)
    return this.pointAtT(t)
  }

  pointAtT(t: number) {
    if (t <= 0) {
      return this.start.clone()
    }

    if (t >= 1) {
      return this.end.clone()
    }

    return this.getSkeletonPoints(t).divider
  }

  isDifferentiable() {
    const start = this.start
    const control1 = this.controlPoint1
    const control2 = this.controlPoint2
    const end = this.end

    return !(
      start.equals(control1) &&
      control1.equals(control2) &&
      control2.equals(end)
    )
  }

  tangentAt(ratio: number, options: Curve.Options = {}) {
    if (!this.isDifferentiable()) return null

    if (ratio < 0) {
      ratio = 0 // eslint-disable-line
    } else if (ratio > 1) {
      ratio = 1 // eslint-disable-line
    }

    const t = this.tAt(ratio, options)
    return this.tangentAtT(t)
  }

  tangentAtLength(length: number, options: Curve.Options = {}) {
    if (!this.isDifferentiable()) {
      return null
    }

    const t = this.tAtLength(length, options)
    return this.tangentAtT(t)
  }

  tangentAtT(t: number) {
    if (!this.isDifferentiable()) {
      return null
    }

    if (t < 0) {
      t = 0 // eslint-disable-line
    }

    if (t > 1) {
      t = 1 // eslint-disable-line
    }

    const skeletonPoints = this.getSkeletonPoints(t)
    const p1 = skeletonPoints.startControlPoint2
    const p2 = skeletonPoints.dividerControlPoint1

    const tangentStart = skeletonPoints.divider
    const tangentLine = new Line(p1, p2)
    // move so that tangent line starts at the point requested
    tangentLine.translate(tangentStart.x - p1.x, tangentStart.y - p1.y)
    return tangentLine
  }

  protected getPrecision(options: Curve.Options = {}) {
    return options.precision == null ? this.PRECISION : options.precision
  }

  protected getDivisions(options: Curve.Options = {}) {
    if (options.subdivisions != null) {
      return options.subdivisions
    }

    const precision = this.getPrecision(options)
    return this.getSubdivisions({ precision })
  }

  protected getOptions(options: Curve.Options = {}): Curve.Options {
    const precision = this.getPrecision(options)
    const subdivisions = this.getDivisions(options)
    return { precision, subdivisions }
  }

  protected tAt(ratio: number, options: Curve.Options = {}) {
    if (ratio <= 0) {
      return 0
    }
    if (ratio >= 1) {
      return 1
    }

    const opts = this.getOptions(options)
    const total = this.length(opts)
    const length = total * ratio
    return this.tAtLength(length, opts)
  }

  protected tAtLength(length: number, options: Curve.Options = {}) {
    let fromStart = true
    if (length < 0) {
      fromStart = false
      length = -length // eslint-disable-line
    }

    const precision = this.getPrecision(options)
    const subdivisions = this.getDivisions(options)
    const opts = { precision, subdivisions }

    let investigatedSubdivision: Curve | null = null
    let investigatedSubdivisionStartT: number
    let investigatedSubdivisionEndT: number
    let baselinePointDistFromStart = 0
    let baselinePointDistFromEnd = 0
    let memo = 0

    const count = subdivisions.length
    let piece = count > 0 ? 1 / count : 0

    for (let i = 0; i < count; i += 1) {
      const index = fromStart ? i : count - 1 - i
      const division = subdivisions[i]
      const dist = division.endpointDistance()

      if (length <= memo + dist) {
        investigatedSubdivision = division
        investigatedSubdivisionStartT = index * piece
        investigatedSubdivisionEndT = (index + 1) * piece

        baselinePointDistFromStart = fromStart
          ? length - memo
          : dist + memo - length
        baselinePointDistFromEnd = fromStart
          ? dist + memo - length
          : length - memo

        break
      }

      memo += dist
    }

    if (investigatedSubdivision == null) {
      return fromStart ? 1 : 0
    }

    // note that precision affects what length is recorded
    // (imprecise measurements underestimate length by up to 10^(-precision) of the precise length)
    // e.g. at precision 1, the length may be underestimated by up to 10% and cause this function to return 1

    const total = this.length(opts)
    const precisionRatio = Math.pow(10, -precision) // eslint-disable-line

    // recursively divide investigated subdivision:
    // until distance between baselinePoint and closest path endpoint is within 10^(-precision)
    // then return the closest endpoint of that final subdivision
    // eslint-disable-next-line
    while (true) {
      let ratio

      ratio = total !== 0 ? baselinePointDistFromStart / total : 0
      if (ratio < precisionRatio) {
        return investigatedSubdivisionStartT!
      }

      ratio = total !== 0 ? baselinePointDistFromEnd / total : 0
      if (ratio < precisionRatio) {
        return investigatedSubdivisionEndT!
      }

      // otherwise, set up for next iteration
      let newBaselinePointDistFromStart
      let newBaselinePointDistFromEnd

      const divided: [Curve, Curve] = investigatedSubdivision.divide(0.5)
      piece /= 2

      const baseline1Length = divided[0].endpointDistance()
      const baseline2Length = divided[1].endpointDistance()

      if (baselinePointDistFromStart <= baseline1Length) {
        investigatedSubdivision = divided[0]
        investigatedSubdivisionEndT! -= piece

        newBaselinePointDistFromStart = baselinePointDistFromStart
        newBaselinePointDistFromEnd =
          baseline1Length - newBaselinePointDistFromStart
      } else {
        investigatedSubdivision = divided[1]
        investigatedSubdivisionStartT! += piece

        newBaselinePointDistFromStart =
          baselinePointDistFromStart - baseline1Length
        newBaselinePointDistFromEnd =
          baseline2Length - newBaselinePointDistFromStart
      }

      baselinePointDistFromStart = newBaselinePointDistFromStart
      baselinePointDistFromEnd = newBaselinePointDistFromEnd
    }
  }

  toPoints(options: Curve.Options = {}) {
    const subdivisions = this.getDivisions(options)
    const points = [subdivisions[0].start.clone()]
    subdivisions.forEach((c) => points.push(c.end.clone()))
    return points
  }

  toPolyline(options: Curve.Options = {}) {
    return new Polyline(this.toPoints(options))
  }

  scale(sx: number, sy: number, origin?: Point.PointLike | Point.PointData) {
    this.start.scale(sx, sy, origin)
    this.controlPoint1.scale(sx, sy, origin)
    this.controlPoint2.scale(sx, sy, origin)
    this.end.scale(sx, sy, origin)
    return this
  }

  rotate(angle: number, origin?: Point.PointLike | Point.PointData) {
    this.start.rotate(angle, origin)
    this.controlPoint1.rotate(angle, origin)
    this.controlPoint2.rotate(angle, origin)
    this.end.rotate(angle, origin)
    return this
  }

  translate(tx: number, ty: number): this
  translate(p: Point.PointLike | Point.PointData): this
  translate(tx: number | Point.PointLike | Point.PointData, ty?: number) {
    if (typeof tx === 'number') {
      this.start.translate(tx, ty as number)
      this.controlPoint1.translate(tx, ty as number)
      this.controlPoint2.translate(tx, ty as number)
      this.end.translate(tx, ty as number)
    } else {
      this.start.translate(tx)
      this.controlPoint1.translate(tx)
      this.controlPoint2.translate(tx)
      this.end.translate(tx)
    }

    return this
  }

  equals(c: Curve) {
    return (
      c != null &&
      this.start.equals(c.start) &&
      this.controlPoint1.equals(c.controlPoint1) &&
      this.controlPoint2.equals(c.controlPoint2) &&
      this.end.equals(c.end)
    )
  }

  clone() {
    return new Curve(
      this.start,
      this.controlPoint1,
      this.controlPoint2,
      this.end,
    )
  }

  toJSON() {
    return {
      start: this.start.toJSON(),
      controlPoint1: this.controlPoint1.toJSON(),
      controlPoint2: this.controlPoint2.toJSON(),
      end: this.end.toJSON(),
    }
  }

  serialize() {
    return [
      this.start.serialize(),
      this.controlPoint1.serialize(),
      this.controlPoint2.serialize(),
      this.end.serialize(),
    ].join(' ')
  }
}

export namespace Curve {
  export const toStringTag = `X6.Geometry.${Curve.name}`

  export function isCurve(instance: any): instance is Curve {
    if (instance == null) {
      return false
    }

    if (instance instanceof Curve) {
      return true
    }

    const tag = instance[Symbol.toStringTag]
    const curve = instance as Curve

    if (
      (tag == null || tag === toStringTag) &&
      Point.isPoint(curve.start) &&
      Point.isPoint(curve.controlPoint1) &&
      Point.isPoint(curve.controlPoint2) &&
      Point.isPoint(curve.end) &&
      typeof curve.toPoints === 'function' &&
      typeof curve.toPolyline === 'function'
    ) {
      return true
    }

    return false
  }
}

export namespace Curve {
  export interface Options {
    precision?: number
    subdivisions?: Curve[]
  }
}
export namespace Curve {
  function getFirstControlPoints(rhs: number[]) {
    const n = rhs.length
    const x = [] // `x` is a solution vector.
    const tmp = []
    let b = 2.0

    x[0] = rhs[0] / b

    // Decomposition and forward substitution.
    for (let i = 1; i < n; i += 1) {
      tmp[i] = 1 / b
      b = (i < n - 1 ? 4.0 : 3.5) - tmp[i]
      x[i] = (rhs[i] - x[i - 1]) / b
    }

    for (let i = 1; i < n; i += 1) {
      // Backsubstitution.
      x[n - i - 1] -= tmp[n - i] * x[n - i]
    }

    return x
  }

  function getCurveControlPoints(
    points: (Point.PointLike | Point.PointData)[],
  ) {
    const knots = points.map((p) => Point.clone(p))
    const firstControlPoints = []
    const secondControlPoints = []
    const n = knots.length - 1

    // Special case: Bezier curve should be a straight line.
    if (n === 1) {
      // 3P1 = 2P0 + P3
      firstControlPoints[0] = new Point(
        (2 * knots[0].x + knots[1].x) / 3,
        (2 * knots[0].y + knots[1].y) / 3,
      )

      // P2 = 2P1 – P0
      secondControlPoints[0] = new Point(
        2 * firstControlPoints[0].x - knots[0].x,
        2 * firstControlPoints[0].y - knots[0].y,
      )

      return [firstControlPoints, secondControlPoints]
    }

    // Calculate first Bezier control points.
    // Right hand side vector.
    const rhs = []

    // Set right hand side X values.
    for (let i = 1; i < n - 1; i += 1) {
      rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x
    }

    rhs[0] = knots[0].x + 2 * knots[1].x
    rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0

    // Get first control points X-values.
    const x = getFirstControlPoints(rhs)

    // Set right hand side Y values.
    for (let i = 1; i < n - 1; i += 1) {
      rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y
    }

    rhs[0] = knots[0].y + 2 * knots[1].y
    rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0

    // Get first control points Y-values.
    const y = getFirstControlPoints(rhs)

    // Fill output arrays.
    for (let i = 0; i < n; i += 1) {
      // First control point.
      firstControlPoints.push(new Point(x[i], y[i]))

      // Second control point.
      if (i < n - 1) {
        secondControlPoints.push(
          new Point(
            2 * knots[i + 1].x - x[i + 1],
            2 * knots[i + 1].y - y[i + 1],
          ),
        )
      } else {
        secondControlPoints.push(
          new Point((knots[n].x + x[n - 1]) / 2, (knots[n].y + y[n - 1]) / 2),
        )
      }
    }

    return [firstControlPoints, secondControlPoints]
  }

  export function throughPoints(points: (Point.PointLike | Point.PointData)[]) {
    if (points == null || (Array.isArray(points) && points.length < 2)) {
      throw new Error('At least 2 points are required')
    }

    const controlPoints = getCurveControlPoints(points)

    const curves = []
    for (let i = 0, ii = controlPoints[0].length; i < ii; i += 1) {
      const controlPoint1 = new Point(
        controlPoints[0][i].x,
        controlPoints[0][i].y,
      )
      const controlPoint2 = new Point(
        controlPoints[1][i].x,
        controlPoints[1][i].y,
      )

      curves.push(
        new Curve(points[i], controlPoint1, controlPoint2, points[i + 1]),
      )
    }

    return curves
  }
}
