import {interpolateRotation} from "./vfx-utils.mjs";

/**
 * @import {VFXBasePathPoint, VFXPathPoint} from "./_types.mjs";
 */

/**
 * A class responsible for constructing a path of points used for animation.
 */
export default class VFXPath {
  /**
   * Construct a VFXPath by providing an array of base point data.
   * @param {VFXBasePathPoint[]} points
   */
  constructor(points) {
    if ( !Array.isArray(points) || (points.length < 2) ) {
      throw new Error("The VFXPath#constructor must be passed an array of points with length of at least 2.");
    }
    this.#distances = new Float32Array(points.length);
    this.#distances[0] = 0;
    this.#pathPoints = new Array(points.length);
    for (let i=0; i<points.length; i++) {
      let {x, y, rotation, elevation=0, sort=0} = points[i];
      const lastPoint = points[i-1] ?? points[i];
      const dx = x - lastPoint.x;
      const dy = y - lastPoint.y;
      const distance = this.#pathLength + Math.hypot(dx, dy);
      this.#distances[i] = distance;
      this.#pathLength = distance;
      if ( (typeof rotation !== "number") && (i > 0) ) rotation = Math.atan2(dy, dx);
      this.#pathPoints[i] = {x, y, rotation: rotation || 0, elevation, sort, distance, index: i};
    }
    this.#pathLength = this.#distances.at(-1) ?? 0;
    this.#pathPoints[0].rotation = this.#pathPoints[1].rotation;
  }

  /**
   * The array of points in the path
   * @type {VFXPathPoint[]}
   */
  get pathPoints() {
    return this.#pathPoints;
  }

  #pathPoints = [];

  /**
   * Discrete path rotation data. Holds distances from the start of the path to each point
   * @type {Float32Array}
   */
  #distances;

  /**
   * Total length of the traveled path across all segments.
   * @type {number}
   */
  #pathLength = 0;

  get pathLength() {
    return this.#pathLength;
  }

  /* -------------------------------------------- */

  /**
   * Get the interpolated point for a value x on [0, 1]
   * @param {number} x        The animation progress on [0, 1]
   * @param {number} index    A known index of the path which was already reached
   * @returns {VFXPathPoint}
   */
  interpolatedPoint(x, index=0) {
    if ( x <= 0 ) return this.#pathPoints[0];
    if ( (x >= 1) || !this.#pathLength ) return this.#pathPoints.at(-1);
    let i;
    for ( i=index; i<this.#pathPoints.length-1; i++ ) {
      if ( x < (this.#pathPoints[i].distance / this.#pathLength) ) break;
    }
    const p0 = this.#pathPoints[i-1];
    const p1 = this.#pathPoints[i];
    const x0 = p0.distance / this.#pathLength;
    const x1 = p1.distance / this.#pathLength;
    const w = (x - x0) / (x1 - x0);
    return {
      x: Math.mix(p0.x, p1.x, w),
      y: Math.mix(p0.y, p1.y, w),
      rotation: interpolateRotation(p0.rotation, p1.rotation, w),
      elevation: p1.elevation,  // Eagerly jump to values of elevation, sort, and sortLayer
      sort: p1.sort,
      sortLayer: p1.sortLayer,
      distance: x * this.#pathLength,
      index: i-1
    };
  }

  /* -------------------------------------------- */

  /**
   * Compute an interpolated point along the path at a given distance.
   * @param {number} distance     The desired distance along the path
   * @returns {VFXPathPoint}      An interpolated point for object position at that distance
   */
  interpolatedPointAtDistance(distance) {
    if ( distance <=0 ) return this.#pathPoints[0];
    if ( !this.#pathLength || (distance > this.#pathLength) ) return this.#pathPoints.at(-1);
    const index = this.#findIndexForDistance(distance);
    const p0 = this.#pathPoints[index];
    const p1 = this.#pathPoints[index + 1];
    if ( !p1 || (p0.distance === distance) ) return p0;
    const ratio = (distance - p0.distance) / (p1.distance - p0.distance);
    return {
      x: Math.mix(p0.x, p1.x, ratio),
      y: Math.mix(p0.y, p1.y, ratio),
      rotation: interpolateRotation(p0.rotation, p1.rotation, ratio),
      elevation: p1.elevation,  // Eagerly jump to values of elevation, sort, and sortLayer
      sort: p1.sort,
      sortLayer: p1.sortLayer,
      distance,
      index
    };
  }

  /* -------------------------------------------- */

  /**
   * Binary search for the index of the closest point in the path that is smaller or equal to the given distance.
   * Trusts that the provided distance is an intermediate value on (0, this.#pathLength).
   * @param {number} distance   A distance along the path that has been traveled
   * @returns {number}          The index of the closest point in the path shorter or equal to the given distance
   */
  #findIndexForDistance(distance) {
    let start = 0;
    let end = this.#distances.length - 1;
    while (start <= end) {
      const mid = (start + end) >> 1;
      if (distance >= this.#distances[mid] && distance < this.#distances[mid + 1]) return mid;
      if (distance < this.#distances[mid]) end = mid - 1;
      else start = mid + 1;
    }
    return this.#distances.length - 1;
  }

  /* -------------------------------------------- */

  /**
   * Get a configured path generator from CONFIG.Canvas.vfx.paths.
   * @param {string} pathName             The named path type
   * @returns {VFXPathGenerator}          The path generator function
   */
  static getPathGenerator(pathName) {
    const pathGenerator = CONFIG.Canvas.vfx.paths[pathName];
    if ( typeof pathGenerator !== "function" ) {
      throw new Error(`VFX path generator function "${pathName}" was not defined in CONFIG.Canvas.vfx.paths`);
    }
    return pathGenerator;
  }

  /* -------------------------------------------- */

  /**
   * Create a VFXPath instance of a certain named path type defined in CONFIG.Canvas.vfx.paths.
   * @param {string} pathName             The named path type to construct
   * @param {VFXBasePathPoint[]} points   Path points to construct
   * @param {object} parameters           Additional parameters used to construct the path
   * @returns {VFXPath}                   The generated path
   */
  static create(pathName, points, parameters={}) {
    const pathGenerator = this.getPathGenerator(pathName);
    return pathGenerator(points, parameters);
  }
}
