/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */


/**
 * @fileoverview Represents a cubic Bezier curve.
 *
 * Uses the deCasteljau algorithm to compute points on the curve.
 * http://en.wikipedia.org/wiki/De_Casteljau's_algorithm
 *
 * Currently it uses an unrolled version of the algorithm for speed.  Eventually
 * it may be useful to use the loop form of the algorithm in order to support
 * curves of arbitrary degree.
 */

goog.provide('goog.math.Bezier');

goog.require('goog.math');
goog.require('goog.math.Coordinate');



/**
 * Object representing a cubic bezier curve.
 * @param {number} x0 X coordinate of the start point.
 * @param {number} y0 Y coordinate of the start point.
 * @param {number} x1 X coordinate of the first control point.
 * @param {number} y1 Y coordinate of the first control point.
 * @param {number} x2 X coordinate of the second control point.
 * @param {number} y2 Y coordinate of the second control point.
 * @param {number} x3 X coordinate of the end point.
 * @param {number} y3 Y coordinate of the end point.
 * @struct
 * @constructor
 * @final
 */
goog.math.Bezier = function(x0, y0, x1, y1, x2, y2, x3, y3) {
  'use strict';
  /**
   * X coordinate of the first point.
   * @type {number}
   */
  this.x0 = x0;

  /**
   * Y coordinate of the first point.
   * @type {number}
   */
  this.y0 = y0;

  /**
   * X coordinate of the first control point.
   * @type {number}
   */
  this.x1 = x1;

  /**
   * Y coordinate of the first control point.
   * @type {number}
   */
  this.y1 = y1;

  /**
   * X coordinate of the second control point.
   * @type {number}
   */
  this.x2 = x2;

  /**
   * Y coordinate of the second control point.
   * @type {number}
   */
  this.y2 = y2;

  /**
   * X coordinate of the end point.
   * @type {number}
   */
  this.x3 = x3;

  /**
   * Y coordinate of the end point.
   * @type {number}
   */
  this.y3 = y3;
};


/**
 * Constant used to approximate ellipses.
 * See: http://canvaspaint.org/blog/2006/12/ellipse/
 * @type {number}
 */
goog.math.Bezier.KAPPA = 4 * (Math.sqrt(2) - 1) / 3;


/**
 * @return {!goog.math.Bezier} A copy of this curve.
 */
goog.math.Bezier.prototype.clone = function() {
  'use strict';
  return new goog.math.Bezier(
      this.x0, this.y0, this.x1, this.y1, this.x2, this.y2, this.x3, this.y3);
};


/**
 * Test if the given curve is exactly the same as this one.
 * @param {goog.math.Bezier} other The other curve.
 * @return {boolean} Whether the given curve is the same as this one.
 */
goog.math.Bezier.prototype.equals = function(other) {
  'use strict';
  return this.x0 == other.x0 && this.y0 == other.y0 && this.x1 == other.x1 &&
      this.y1 == other.y1 && this.x2 == other.x2 && this.y2 == other.y2 &&
      this.x3 == other.x3 && this.y3 == other.y3;
};


/**
 * Modifies the curve in place to progress in the opposite direction.
 */
goog.math.Bezier.prototype.flip = function() {
  'use strict';
  var temp = this.x0;
  this.x0 = this.x3;
  this.x3 = temp;
  temp = this.y0;
  this.y0 = this.y3;
  this.y3 = temp;

  temp = this.x1;
  this.x1 = this.x2;
  this.x2 = temp;
  temp = this.y1;
  this.y1 = this.y2;
  this.y2 = temp;
};


/**
 * Computes the curve's X coordinate at a point between 0 and 1.
 * @param {number} t The point on the curve to find.
 * @return {number} The computed coordinate.
 */
goog.math.Bezier.prototype.getPointX = function(t) {
  'use strict';
  // Special case start and end.
  if (t == 0) {
    return this.x0;
  } else if (t == 1) {
    return this.x3;
  }

  // Step one - from 4 points to 3
  var ix0 = goog.math.lerp(this.x0, this.x1, t);
  var ix1 = goog.math.lerp(this.x1, this.x2, t);
  var ix2 = goog.math.lerp(this.x2, this.x3, t);

  // Step two - from 3 points to 2
  ix0 = goog.math.lerp(ix0, ix1, t);
  ix1 = goog.math.lerp(ix1, ix2, t);

  // Final step - last point
  return goog.math.lerp(ix0, ix1, t);
};


/**
 * Computes the curve's Y coordinate at a point between 0 and 1.
 * @param {number} t The point on the curve to find.
 * @return {number} The computed coordinate.
 */
goog.math.Bezier.prototype.getPointY = function(t) {
  'use strict';
  // Special case start and end.
  if (t == 0) {
    return this.y0;
  } else if (t == 1) {
    return this.y3;
  }

  // Step one - from 4 points to 3
  var iy0 = goog.math.lerp(this.y0, this.y1, t);
  var iy1 = goog.math.lerp(this.y1, this.y2, t);
  var iy2 = goog.math.lerp(this.y2, this.y3, t);

  // Step two - from 3 points to 2
  iy0 = goog.math.lerp(iy0, iy1, t);
  iy1 = goog.math.lerp(iy1, iy2, t);

  // Final step - last point
  return goog.math.lerp(iy0, iy1, t);
};


/**
 * Computes the curve at a point between 0 and 1.
 * @param {number} t The point on the curve to find.
 * @return {!goog.math.Coordinate} The computed coordinate.
 */
goog.math.Bezier.prototype.getPoint = function(t) {
  'use strict';
  return new goog.math.Coordinate(this.getPointX(t), this.getPointY(t));
};


/**
 * Changes this curve in place to be the portion of itself from [t, 1].
 * @param {number} t The start of the desired portion of the curve.
 */
goog.math.Bezier.prototype.subdivideLeft = function(t) {
  'use strict';
  if (t == 1) {
    return;
  }

  // Step one - from 4 points to 3
  var ix0 = goog.math.lerp(this.x0, this.x1, t);
  var iy0 = goog.math.lerp(this.y0, this.y1, t);

  var ix1 = goog.math.lerp(this.x1, this.x2, t);
  var iy1 = goog.math.lerp(this.y1, this.y2, t);

  var ix2 = goog.math.lerp(this.x2, this.x3, t);
  var iy2 = goog.math.lerp(this.y2, this.y3, t);

  // Collect our new x1 and y1
  this.x1 = ix0;
  this.y1 = iy0;

  // Step two - from 3 points to 2
  ix0 = goog.math.lerp(ix0, ix1, t);
  iy0 = goog.math.lerp(iy0, iy1, t);

  ix1 = goog.math.lerp(ix1, ix2, t);
  iy1 = goog.math.lerp(iy1, iy2, t);

  // Collect our new x2 and y2
  this.x2 = ix0;
  this.y2 = iy0;

  // Final step - last point
  this.x3 = goog.math.lerp(ix0, ix1, t);
  this.y3 = goog.math.lerp(iy0, iy1, t);
};


/**
 * Changes this curve in place to be the portion of itself from [0, t].
 * @param {number} t The end of the desired portion of the curve.
 */
goog.math.Bezier.prototype.subdivideRight = function(t) {
  'use strict';
  this.flip();
  this.subdivideLeft(1 - t);
  this.flip();
};


/**
 * Changes this curve in place to be the portion of itself from [s, t].
 * @param {number} s The start of the desired portion of the curve.
 * @param {number} t The end of the desired portion of the curve.
 */
goog.math.Bezier.prototype.subdivide = function(s, t) {
  'use strict';
  this.subdivideRight(s);
  this.subdivideLeft((t - s) / (1 - s));
};


/**
 * Computes the position t of a point on the curve given its x coordinate.
 * That is, for an input xVal, finds t s.t. getPointX(t) = xVal.
 * As such, the following should always be true up to some small epsilon:
 * t ~ solvePositionFromXValue(getPointX(t)) for t in [0, 1].
 * @param {number} xVal The x coordinate of the point to find on the curve.
 * @return {number} The position t.
 */
goog.math.Bezier.prototype.solvePositionFromXValue = function(xVal) {
  'use strict';
  // Desired precision on the computation.
  var epsilon = 1e-6;

  // Initial estimate of t using linear interpolation.
  var t = (xVal - this.x0) / (this.x3 - this.x0);
  if (t <= 0) {
    return 0;
  } else if (t >= 1) {
    return 1;
  }

  // Try gradient descent to solve for t. If it works, it is very fast.
  var tMin = 0;
  var tMax = 1;
  var value = 0;
  for (var i = 0; i < 8; i++) {
    value = this.getPointX(t);
    var derivative = (this.getPointX(t + epsilon) - value) / epsilon;
    if (Math.abs(value - xVal) < epsilon) {
      return t;
    } else if (Math.abs(derivative) < epsilon) {
      break;
    } else {
      if (value < xVal) {
        tMin = t;
      } else {
        tMax = t;
      }
      t -= (value - xVal) / derivative;
    }
  }

  // If the gradient descent got stuck in a local minimum, e.g. because
  // the derivative was close to 0, use a Dichotomy refinement instead.
  // We limit the number of interations to 8.
  for (var i = 0; Math.abs(value - xVal) > epsilon && i < 8; i++) {
    if (value < xVal) {
      tMin = t;
      t = (t + tMax) / 2;
    } else {
      tMax = t;
      t = (t + tMin) / 2;
    }
    value = this.getPointX(t);
  }
  return t;
};


/**
 * Computes the y coordinate of a point on the curve given its x coordinate.
 * @param {number} xVal The x coordinate of the point on the curve.
 * @return {number} The y coordinate of the point on the curve.
 */
goog.math.Bezier.prototype.solveYValueFromXValue = function(xVal) {
  'use strict';
  return this.getPointY(this.solvePositionFromXValue(xVal));
};
