/**
 * A simple linear color gradient implementation.
 *
 * A color gradient is just an array of color objects paired with position values on the gradient.
 *
 * TODO: add `angle`
 *
 * @author  Ikaros Kappler
 * @version 1.0.0
 * @date    2025-08-05
 * @modified 2025-11-26 Fixing a bug in `ColorGradient.getColorAt(ratio)`: value at 1.0 caused runtime error.
 */

import { Color } from "./Color";

const RAD_TO_DEG: number = 180.0 / Math.PI;

export type ColorGradientRotation = number | `${number}deg` | `${number}turn` | `to top` | "to bottom" | "to left" | "to right";

const DEFAULT_COLORSET: Array<ColorGradientItem> = [
  { color: Color.Red, ratio: 0.0 },
  { color: Color.Gold, ratio: 0.2 },
  { color: Color.Yellow, ratio: 0.4 },
  { color: Color.LimeGreen, ratio: 0.6 },
  { color: Color.MediumBlue, ratio: 0.8 },
  { color: Color.Purple, ratio: 1.0 }
];

export interface ColorGradientItem {
  color: Color;
  ratio: number; // Ideally a percentage value in [0.0 .. 1.0]
}

export class ColorGradient {
  values: Array<ColorGradientItem>;
  angle: ColorGradientRotation;

  static DEFAULT_COLORSET = DEFAULT_COLORSET;

  /**
   * Creates a new ColorGradient with the given values. Please be sure that the values appear in the correct
   * order regarding the `ratio` information. This will not be validated.
   *
   * @param {Array<ColorGradientItem>} values - The values to use.
   * @param {number?} angle - (optional) An optional angle for the gradient; if no specified `PI/2.0` will be used (vertical from left to right).
   */
  constructor(values: Array<ColorGradientItem>, angle?: ColorGradientRotation) {
    this.values = values;
    // Default: left to right
    this.angle = typeof angle === "undefined" ? Math.PI / 2.0 : angle;
  }

  /**
   * Get a color gradient CSS value string from these gradient settings.
   *
   * @instance
   * @memberof ColorGradientPicker
   * @returns {string}
   */
  public toColorGradientString(): string {
    // Example:
    //    linear-gradient(90deg,rgba(42, 123, 155, 1) 0%, rgba(87, 199, 133, 1) 38%, rgba(161, 210, 108, 1) 68%, rgba(237, 221, 83, 1) 100%)

    // const buffer: Array<string | undefined> = [`linear-gradient( ${RAD_TO_DEG * this.angle}deg, `];
    const buffer: Array<string | undefined> = [
      `linear-gradient( ${typeof this.angle === "number" ? `${this.angle * RAD_TO_DEG}deg` : this.angle}, `
    ];

    for (var i = 0; i < this.values.length; i++) {
      if (i > 0) {
        buffer.push(",");
      }
      const colorValue: string = this.values[i] ? this.values[i].color.cssRGBA() : null;
      buffer.push(colorValue);
      const percentage = this.values[i].ratio;
      buffer.push(`${typeof percentage === "number" ? `${percentage * 100}%` : percentage}`);
    }
    buffer.push(")");

    return buffer.join(" ");
  }

  /**
   * Get the color at the specific relative position.
   *
   * @param {number} ratio - Any value between 0.0 and 1.0.
   */
  getColorAt(ratio: number): Color {
    // Locate interval first
    const intervalLocation: [number, ColorGradientItem] = this.locateClosestRatio(ratio);
    const leftItem: ColorGradientItem = this.values[intervalLocation[0]];
    const rightItem: ColorGradientItem = this.values[intervalLocation[0] + 1];
    if (!rightItem) {
      return leftItem.color.clone();
    }
    const relativeInnerIntervalPosition: number = (ratio - leftItem.ratio) / (rightItem.ratio - leftItem.ratio);
    return leftItem.color.clone().interpolate(rightItem.color, relativeInnerIntervalPosition);
  }

  /**
   * Find that gradient record (index) that's value is closest to the given relative value. The function will return
   * the closest value and the left index, indicating the containing interval index.
   *
   * @param {number} ratio - The value to look for.
   * @returns {[number,number]} A pair of left interval boundary index and closest value.
   */
  locateClosestRatio(ratio: number): [number, ColorGradientItem] {
    if (this.values.length === 0) {
      console.warn("[Warn] All slider values array is empty. This should not happen, cannot proceed.");
      return [NaN, null]; // This should not happen: at least two values must be present in a gradient
    }
    // console.log("__locateClosestSliderValue", "allSliderValues", allSliderValues);
    let leftSliderIndex: number = 0;
    let closestSliderValue: ColorGradientItem = this.values[leftSliderIndex];
    for (var i = 1; i < this.values.length; i++) {
      const curVal: ColorGradientItem = this.values[i];
      if (Math.abs(curVal.ratio - ratio) < Math.abs(closestSliderValue.ratio - ratio)) {
        closestSliderValue = curVal;
        if (curVal.ratio > ratio) {
          leftSliderIndex = i - 1;
        } else {
          leftSliderIndex = i;
        }
      }
    }
    return [leftSliderIndex, closestSliderValue];
  }

  /**
   * Try to convert the given anonymous item (usually and object) to a ColorGradient. This method should be used
   * to restore items from JSON parse results to convert them into proper ColorGradient instances.
   *
   * @param {any} item
   * @returns {ColorGradient}
   */
  public static fromObject(item: any): ColorGradient {
    if (typeof item !== "object") {
      throw new Error(`Cannot instantiate ColorGradient from item: must be an object. (${typeof item})`);
    }
    if (!item.hasOwnProperty("values")) {
      throw new Error("Cannot instantiate ColorGradient from object: missing property 'values'.");
    }
    const valuesObj: Array<object> = item.values;
    if (!Array.isArray(valuesObj)) {
      throw new Error("Cannot instantiate ColorGradient from object: property 'values' is not an array.");
    }
    const values: Array<ColorGradientItem> = valuesObj.map((element: object, index: number) => {
      if (typeof element !== "object") {
        throw new Error(`Cannot instantiate ColorGradient from object: item at index ${index} is not an object.`);
      }
      if (!element.hasOwnProperty("color") || !element.hasOwnProperty("ratio")) {
        throw new Error(
          `Cannot instantiate ColorGradient from object: item at index ${index} is missing 'color' or 'ratio' property.`
        );
      }
      const tmpElement = element as ColorGradientItem;
      return { color: Color.makeRGB(tmpElement.color.r, tmpElement.color.g, tmpElement.color.b), ratio: tmpElement.ratio };
    });
    // More checks possible here ...
    return new ColorGradient(values, item.angle);
  }

  /**
   * Clone this linear color gradient. Returns a deep clone.
   *
   * @returns {ColorGradient}
   */
  public clone(): ColorGradient {
    return new ColorGradient(
      this.values.map((item: ColorGradientItem) => {
        return { color: item.color, ratio: item.ratio };
      }),
      this.angle
    );
  }

  /**
   * Create a default color gradient with six color: red, orange, yellow, green, blue, purple.
   * @returns
   */
  public static createDefault(): ColorGradient {
    return new ColorGradient(DEFAULT_COLORSET.map((item: ColorGradientItem) => ({ color: item.color, ratio: item.ratio })));
  }

  /**
   * Create a basic color gradient from only two colors.
   *
   * @param {Color} startColor - The leftmost color.
   * @param {Color} endColor - The rightmost color.
   * @param {number?} angleInRadians - (optional) An optional angle for the gradient; if no specified `PI/2.0` will be used (vertical from left to right).
   * @returns
   */
  public static createFrom(startColor: Color, endColor: Color, angleInRadians?: number): ColorGradient {
    return new ColorGradient(
      [
        { ratio: 0.0, color: startColor },
        { ratio: 1.0, color: endColor }
      ],
      angleInRadians
    );
  }
}
