/**
 * Implementation of elliptic sectors.
 * Note that sectors are constructed in clockwise direction.
 *
 * @author   Ikaros Kappler
 * @date     2021-02-26
 * @modified 2022-02-02 Added the `destroy` method.
 * @modified 2022-11-01 Tweaked the `endpointToCenterParameters` function to handle negative values, too, without errors.
 * @version  1.1.1
 */

import { CubicBezierCurve } from "./CubicBezierCurve";
import { geomutils } from "./geomutils";
import { SVGPathParams, UID, XYCoords } from "./interfaces";
import { Line } from "./Line";
import { UIDGenerator } from "./UIDGenerator";
import { Vector } from "./Vector";
import { VEllipse } from "./VEllipse";
import { Vertex } from "./Vertex";

/**
 * @classdesc A class for elliptic sectors.
 *
 * @requires Line
 * @requires Vector
 * @requires VertTuple
 * @requires Vertex
 * @requires SVGSerializale
 * @requires UID
 * @requires UIDGenerator
 **/
export class VEllipseSector {
  /**
   * Required to generate proper CSS classes and other class related IDs.
   **/
  readonly className: string = "VEllipseSector";

  /**
   * The UID of this drawable object.
   *
   * @member {UID}
   * @memberof VEllipseSector
   * @instance
   * @readonly
   */
  readonly uid: UID;

  /**
   * @member {VEllipse}
   * @memberof VEllipseSector
   * @instance
   */
  ellipse: VEllipse;

  /**
   * @member {number}
   * @memberof VEllipseSector
   * @instance
   */
  startAngle: number;

  /**
   * @member {number}
   * @memberof VEllipseSector
   * @instance
   */
  endAngle: number;

  /**
   * @member isDestroyed
   * @memberof VEllipseSector
   * @type {boolean}
   * @instance
   */
  isDestroyed: boolean;

  /**
   * Create a new elliptic sector from the given ellipse and two angles.
   *
   * Note that the direction from start to end goes clockwise, and that start and end angle
   * will be wrapped to [0,PI*2).
   *
   * @constructor
   * @name VEllipseSector
   * @param {VEllipse} - The underlying ellipse to use.
   * @param {number} startAngle - The start angle of the sector.
   * @param {numner} endAngle - The end angle of the sector.
   */
  constructor(ellipse: VEllipse, startAngle: number, endAngle: number) {
    this.uid = UIDGenerator.next();
    this.ellipse = ellipse;
    this.startAngle = geomutils.wrapMinMax(startAngle, 0, Math.PI * 2);
    this.endAngle = geomutils.wrapMinMax(endAngle, 0, Math.PI * 2);
  }

  /**
   * Convert this elliptic sector into cubic Bézier curves.
   *
   * @param {number=3} quarterSegmentCount - The number of segments per base elliptic quarter (default is 3, min is 1).
   * @param {number=0.666666} threshold - The Bézier threshold (default value 0.666666 approximates the ellipse with best results
   * but you might wish to use other values)
   * @return {Array<CubicBezierCurve>} An array of cubic Bézier curves representing the elliptic sector.
   */
  toCubicBezier(quarterSegmentCount?: number, threshold?: number): Array<CubicBezierCurve> {
    // There are at least 4 segments required (dour quarters) to approximate a whole
    // ellipse with Bézier curves.
    // A visually 'good' approximation should have 12; this seems to be a good value (anything multiple of 4).
    const segmentCount = Math.max(1, quarterSegmentCount || 3) * 4;
    threshold = typeof threshold === "undefined" ? 0.666666 : threshold;

    const radiusH: number = this.ellipse.radiusH();
    const radiusV: number = this.ellipse.radiusV();

    var startAngle = VEllipseSector.ellipseSectorUtils.normalizeAngle(this.startAngle);
    var endAngle = VEllipseSector.ellipseSectorUtils.normalizeAngle(this.endAngle);

    // Find all angles inside start and end
    var angles = VEllipseSector.ellipseSectorUtils.equidistantVertAngles(radiusH, radiusV, startAngle, endAngle, segmentCount);
    angles = [startAngle].concat(angles).concat([endAngle]);

    const curves: Array<CubicBezierCurve> = [];
    let curAngle: number = angles[0];
    let startPoint: Vertex = this.ellipse.vertAt(curAngle);
    for (var i = 0; i + 1 < angles.length; i++) {
      let nextAngle = angles[(i + 1) % angles.length];
      let endPoint: Vertex = this.ellipse.vertAt(nextAngle);

      let startTangent: Vector = this.ellipse.tangentAt(curAngle);
      let endTangent: Vector = this.ellipse.tangentAt(nextAngle);

      // Distorted ellipses can only be approximated by linear Bézier segments
      if (Math.abs(radiusV) < 0.0001 || Math.abs(radiusH) < 0.0001) {
        let diff = startPoint.difference(endPoint);
        let curve: CubicBezierCurve = new CubicBezierCurve(
          startPoint.clone(),
          endPoint.clone(),
          startPoint.clone().addXY(diff.x * 0.333, diff.y * 0.333),
          endPoint.clone().addXY(-diff.x * 0.333, -diff.y * 0.333)
        );
        curves.push(curve);
      } else {
        // Find intersection
        let intersection: Vertex | null = startTangent.intersection(endTangent);
        // What if intersection is undefined?
        // --> This *can* not happen if segmentCount > 2 and height and width of the ellipse are not zero.

        if (intersection) {
          // It's VERY LIKELY hat this ALWAYS happens; it's just a typesave variant.
          // Intersection cannot be null.
          let startDiff: Vertex = startPoint.difference(intersection);
          let endDiff: Vertex = endPoint.difference(intersection);
          let curve: CubicBezierCurve = new CubicBezierCurve(
            startPoint.clone(),
            endPoint.clone(),
            startPoint.clone().add(startDiff.scale(threshold)),
            endPoint.clone().add(endDiff.scale(threshold))
          );
          curves.push(curve);
        }
      }
      startPoint = endPoint;
      curAngle = nextAngle;
    }
    return curves;
  }

  /**
   * This function should invalidate any installed listeners and invalidate this object.
   * After calling this function the object might not hold valid data any more and
   * should not be used.
   */
  destroy() {
    this.ellipse.destroy();
    this.isDestroyed = true;
  }

  static ellipseSectorUtils = {
    /**
     * Helper function to convert an elliptic section to SVG arc params (for the `d` attribute).
     * Inspiration found at:
     *    https://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle
     *
     * @param {boolean} options.moveToStart - If false (default=true) the initial 'Move' command will not be used.
     * @return [ 'A', radiusH, radiusV, rotation, largeArcFlag=1|0, sweepFlag=0, endx, endy ]
     */
    describeSVGArc: (
      x: number,
      y: number,
      radiusH: number,
      radiusV: number,
      startAngle: number,
      endAngle: number,
      rotation?: number,
      options?: { moveToStart: boolean }
    ): SVGPathParams => {
      if (typeof options === "undefined") options = { moveToStart: true };
      if (typeof rotation === "undefined") rotation = 0.0;

      // Important note: this function only works if start- and end-angle are within
      // one whole circle [x,x+2*PI].
      // Revelations of more than 2*PI might result in unexpected arcs.
      // -> Use the geomutils.wrapMax( angle, 2*PI )
      startAngle = geomutils.wrapMax(startAngle, Math.PI * 2);
      endAngle = geomutils.wrapMax(endAngle, Math.PI * 2);

      // Find the start- and end-point on the rotated ellipse
      // XYCoords to Vertex (for rotation)
      var end: Vertex = new Vertex(VEllipse.utils.polarToCartesian(x, y, radiusH, radiusV, endAngle));
      var start: Vertex = new Vertex(VEllipse.utils.polarToCartesian(x, y, radiusH, radiusV, startAngle));
      end.rotate(rotation, { x: x, y: y });
      start.rotate(rotation, { x: x, y: y });

      // Boolean stored as integers (0|1).
      var diff: number = endAngle - startAngle;
      var largeArcFlag: number;
      if (diff < 0) {
        largeArcFlag = Math.abs(diff) < Math.PI ? 1 : 0;
      } else {
        largeArcFlag = Math.abs(diff) > Math.PI ? 1 : 0;
      }

      const sweepFlag: number = 1;
      const pathData: SVGPathParams = [];
      if (options.moveToStart) {
        pathData.push("M", start.x, start.y);
      }
      // Arc rotation in degrees, not radians.
      const r2d: number = 180 / Math.PI;
      pathData.push("A", radiusH, radiusV, rotation * r2d, largeArcFlag, sweepFlag, end.x, end.y);
      return pathData;
    }, // END function describeSVGArc

    /**
     * Helper function to find second-kind elliptic angles, so that the euclidean distance along the the
     * elliptic sector is the same for all.
     *
     * Note that this is based on the full ellipse calculuation and start and end will be cropped; so the
     * distance from the start angle to the first angle and/or the distance from the last angle to
     * the end angle may be different to the others.
     *
     * Furthermore the computation is only possible on un-rotated ellipses; if your source ellipse has
     * a rotation on the plane please 'rotate' the result angles afterwards to find matching angles.
     *
     * Returned angles are normalized to the interval `[ 0, PI*2 ]`.
     *
     * @param {number} radiusH - The first (horizonal) radius of the ellipse.
     * @param {number} radiusV - The second (vertical) radius of the ellipse.
     * @param {number} startAngle - The opening angle of your elliptic sector (please use normalized angles).
     * @param {number} endAngle - The closing angle of your elliptic sector (please use normalized angles).
     * @param {number} fullEllipsePointCount - The number of base segments to use from the source ellipse (12 or 16 are good numbers).
     * @return {Array<number>} An array of n angles inside startAngle and endAngle (where n <= fullEllipsePointCount).
     */
    equidistantVertAngles: (
      radiusH: number,
      radiusV: number,
      startAngle: number,
      endAngle: number,
      fullEllipsePointCount: number
    ): Array<number> => {
      var ellipseAngles = VEllipse.utils.equidistantVertAngles(radiusH, radiusV, fullEllipsePointCount);
      ellipseAngles = ellipseAngles.map((angle: number) => VEllipseSector.ellipseSectorUtils.normalizeAngle(angle));

      var angleIsInRange = (angle: number) => {
        if (startAngle < endAngle) return angle >= startAngle && angle <= endAngle;
        else return angle >= startAngle || (angle <= endAngle && angle >= 0);
      };
      // Drop all angles outside the sector
      var ellipseAngles = ellipseAngles.filter(angleIsInRange);

      // Now we need to sort the angles to the first one in the array is the closest to startAngle.
      // --> find the angle that is closest to the start angle
      var startIndex = VEllipseSector.ellipseSectorUtils.findClosestToStartAngle(startAngle, endAngle, ellipseAngles);

      // Bring all angles into the correct order
      //    Idea: use splice or slice here?
      var angles: Array<number> = [];
      for (var i = 0; i < ellipseAngles.length; i++) {
        angles.push(ellipseAngles[(startIndex + i) % ellipseAngles.length]);
      }

      return angles;
    },

    findClosestToStartAngle: (startAngle: number, endAngle: number, ellipseAngles: Array<number>): number => {
      // Note: endAngle > 0 && startAngle > 0
      if (startAngle > endAngle) {
        const n: number = ellipseAngles.length;
        for (var i = 0; i < n; i++) {
          const ea: number = geomutils.wrapMinMax(ellipseAngles[i], 0, Math.PI * 2);
          if (ea >= startAngle && ea >= endAngle) {
            return i;
          }
        }
      }
      return 0;
    },

    normalizeAngle: (angle: number): number => (angle < 0 ? Math.PI * 2 + angle : angle),

    /**
     * Convert the elliptic arc from endpoint parameters to center parameters as described
     * in the w3c svg arc implementation note.
     *
     * https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
     *
     * @param {number} x1 - The x component of the start point (end of last SVG command).
     * @param {number} y1 - The y component of the start point (end of last SVG command).
     * @param {number} rx - The first (horizontal) radius of the ellipse.
     * @param {number} ry - The second (vertical) radius of the ellipse.
     * @param {number} phi - The ellipse's rotational angle (angle of axis rotation) in radians (not in degrees as the SVG command uses!)
     * @param {boolean} fa - The large-arc-flag (boolean, not 0 or 1).
     * @param {boolean} fs - The sweep-flag (boolean, not 0 or 1).
     * @param {number} x2 - The x component of the end point (end of last SVG command).
     * @param {number} y2 - The y component of the end point (end of last SVG command).
     * @returns
     */
    endpointToCenterParameters(
      x1: number,
      y1: number,
      rx: number,
      ry: number,
      phi: number,
      fa: boolean,
      fs: boolean,
      x2: number,
      y2: number
    ): VEllipseSector {
      // console.log("endpointToCenterParameters", x1, y1, phi, rx, ry, fa, fs, x2, y2);
      // Thanks to
      //    https://observablehq.com/@toja/ellipse-and-elliptical-arc-conversion
      const abs = Math.abs;
      const sin = Math.sin;
      const cos = Math.cos;
      const sqrt = Math.sqrt;
      const pow = (n: number): number => {
        return n * n;
      };

      const sinphi: number = sin(phi);
      const cosphi: number = cos(phi);

      // Step 1: simplify through translation/rotation
      const x: number = (cosphi * (x1 - x2)) / 2 + (sinphi * (y1 - y2)) / 2;
      const y: number = (-sinphi * (x1 - x2)) / 2 + (cosphi * (y1 - y2)) / 2;

      const px: number = pow(x),
        py: number = pow(y),
        prx: number = pow(rx),
        pry: number = pow(ry);

      // correct of out-of-range radii
      const L: number = px / prx + py / pry;
      if (L > 1) {
        rx = sqrt(L) * abs(rx);
        ry = sqrt(L) * abs(ry);
      } else {
        rx = abs(rx);
        ry = abs(ry);
      }

      // Step 2 + 3: compute center
      const sign: number = fa === fs ? -1 : 1;
      // const M: number = sqrt((prx * pry - prx * py - pry * px) / (prx * py + pry * px)) * sign;
      const M: number = sqrt(Math.abs((prx * pry - prx * py - pry * px) / (prx * py + pry * px))) * sign;

      const _cx: number = (M * (rx * y)) / ry;
      const _cy: number = (M * (-ry * x)) / rx;

      const cx: number = cosphi * _cx - sinphi * _cy + (x1 + x2) / 2;
      const cy: number = sinphi * _cx + cosphi * _cy + (y1 + y2) / 2;

      // Step 4: Compute start and end angle
      const center: Vertex = new Vertex(cx, cy);
      const axis: Vertex = center.clone().addXY(rx, ry);
      const ellipse: VEllipse = new VEllipse(center, axis, 0);
      // console.log("VELLIPSE::::::", ellipse);
      ellipse.rotate(phi);

      const startAngle: number = new Line(ellipse.center, new Vertex(x1, y1)).angle();
      const endAngle: number = new Line(ellipse.center, new Vertex(x2, y2)).angle();
      return new VEllipseSector(ellipse, startAngle - phi, endAngle - phi);
    }
  }; // END ellipseSectorUtils
}
