import { cache } from '../../cache';
import { config } from '../../config';
import { halfPI, PiBy180 } from '../../constants';
import type { TMat2D, TRadian, TRectBounds } from '../../typedefs';
import { cos } from '../misc/cos';
import { multiplyTransformMatrices, transformPoint } from '../misc/matrix';
import { sin } from '../misc/sin';
import { toFixed } from '../misc/toFixed';
import type {
  TCurveInfo,
  TComplexPathData,
  TParsedAbsoluteCubicCurveCommand,
  TPathSegmentInfo,
  TPointAngle,
  TSimpleParsedCommand,
  TSimplePathData,
  TPathSegmentCommandInfo,
  TComplexParsedCommand,
  TPathSegmentInfoCommon,
  TEndPathInfo,
  TParsedArcCommand,
  TComplexParsedCommandType,
} from './typedefs';
import type { XY } from '../../Point';
import { Point } from '../../Point';
import { reArcCommandPoints, rePathCommand } from './regex';
import { reNum } from '../../parser/constants';

/**
 * Commands that may be repeated
 */
const repeatedCommands: Record<string, 'l' | 'L'> = {
  m: 'l',
  M: 'L',
};

/**
 * Convert an arc of a rotated ellipse to a Bezier Curve
 * @param {TRadian} theta1 start of the arc
 * @param {TRadian} theta2 end of the arc
 * @param cosTh cosine of the angle of rotation
 * @param sinTh sine of the angle of rotation
 * @param rx x-axis radius (before rotation)
 * @param ry y-axis radius (before rotation)
 * @param cx1 center x of the ellipse
 * @param cy1 center y of the ellipse
 * @param mT
 * @param fromX starting point of arc x
 * @param fromY starting point of arc y
 */
const segmentToBezier = (
  theta1: TRadian,
  theta2: TRadian,
  cosTh: number,
  sinTh: number,
  rx: number,
  ry: number,
  cx1: number,
  cy1: number,
  mT: number,
  fromX: number,
  fromY: number,
): TParsedAbsoluteCubicCurveCommand => {
  const costh1 = cos(theta1),
    sinth1 = sin(theta1),
    costh2 = cos(theta2),
    sinth2 = sin(theta2),
    toX = cosTh * rx * costh2 - sinTh * ry * sinth2 + cx1,
    toY = sinTh * rx * costh2 + cosTh * ry * sinth2 + cy1,
    cp1X = fromX + mT * (-cosTh * rx * sinth1 - sinTh * ry * costh1),
    cp1Y = fromY + mT * (-sinTh * rx * sinth1 + cosTh * ry * costh1),
    cp2X = toX + mT * (cosTh * rx * sinth2 + sinTh * ry * costh2),
    cp2Y = toY + mT * (sinTh * rx * sinth2 - cosTh * ry * costh2);

  return ['C', cp1X, cp1Y, cp2X, cp2Y, toX, toY];
};

/**
 * Adapted from {@link http://dxr.mozilla.org/mozilla-central/source/dom/svg/SVGPathDataParser.cpp}
 * by Andrea Bogazzi code is under MPL. if you don't have a copy of the license you can take it here
 * http://mozilla.org/MPL/2.0/
 * @param toX
 * @param toY
 * @param rx
 * @param ry
 * @param {number} large 0 or 1 flag
 * @param {number} sweep 0 or 1 flag
 * @param rotateX
 */
const arcToSegments = (
  toX: number,
  toY: number,
  rx: number,
  ry: number,
  large: number,
  sweep: number,
  rotateX: TRadian,
): TParsedAbsoluteCubicCurveCommand[] => {
  if (rx === 0 || ry === 0) {
    return [];
  }
  let fromX = 0,
    fromY = 0,
    root = 0;
  const PI = Math.PI,
    theta = rotateX * PiBy180,
    sinTheta = sin(theta),
    cosTh = cos(theta),
    px = 0.5 * (-cosTh * toX - sinTheta * toY),
    py = 0.5 * (-cosTh * toY + sinTheta * toX),
    rx2 = rx ** 2,
    ry2 = ry ** 2,
    py2 = py ** 2,
    px2 = px ** 2,
    pl = rx2 * ry2 - rx2 * py2 - ry2 * px2;
  let _rx = Math.abs(rx);
  let _ry = Math.abs(ry);

  if (pl < 0) {
    const s = Math.sqrt(1 - pl / (rx2 * ry2));
    _rx *= s;
    _ry *= s;
  } else {
    root =
      (large === sweep ? -1.0 : 1.0) * Math.sqrt(pl / (rx2 * py2 + ry2 * px2));
  }

  const cx = (root * _rx * py) / _ry,
    cy = (-root * _ry * px) / _rx,
    cx1 = cosTh * cx - sinTheta * cy + toX * 0.5,
    cy1 = sinTheta * cx + cosTh * cy + toY * 0.5;
  let mTheta = calcVectorAngle(1, 0, (px - cx) / _rx, (py - cy) / _ry);
  let dtheta = calcVectorAngle(
    (px - cx) / _rx,
    (py - cy) / _ry,
    (-px - cx) / _rx,
    (-py - cy) / _ry,
  );

  if (sweep === 0 && dtheta > 0) {
    dtheta -= 2 * PI;
  } else if (sweep === 1 && dtheta < 0) {
    dtheta += 2 * PI;
  }

  // Convert into cubic bezier segments <= 90deg
  const segments = Math.ceil(Math.abs((dtheta / PI) * 2)),
    result = [],
    mDelta = dtheta / segments,
    mT =
      ((8 / 3) * Math.sin(mDelta / 4) * Math.sin(mDelta / 4)) /
      Math.sin(mDelta / 2);
  let th3 = mTheta + mDelta;

  for (let i = 0; i < segments; i++) {
    result[i] = segmentToBezier(
      mTheta,
      th3,
      cosTh,
      sinTheta,
      _rx,
      _ry,
      cx1,
      cy1,
      mT,
      fromX,
      fromY,
    );
    fromX = result[i][5];
    fromY = result[i][6];
    mTheta = th3;
    th3 += mDelta;
  }
  return result;
};

/**
 * @private
 * Calculate the angle between two vectors
 * @param ux u endpoint x
 * @param uy u endpoint y
 * @param vx v endpoint x
 * @param vy v endpoint y
 */
const calcVectorAngle = (
  ux: number,
  uy: number,
  vx: number,
  vy: number,
): TRadian => {
  const ta = Math.atan2(uy, ux),
    tb = Math.atan2(vy, vx);
  if (tb >= ta) {
    return tb - ta;
  } else {
    return 2 * Math.PI - (ta - tb);
  }
};

// functions for the Cubic beizer
// taken from: https://github.com/konvajs/konva/blob/7.0.5/src/shapes/Path.ts#L350
const CB1 = (t: number) => t ** 3;
const CB2 = (t: number) => 3 * t ** 2 * (1 - t);
const CB3 = (t: number) => 3 * t * (1 - t) ** 2;
const CB4 = (t: number) => (1 - t) ** 3;

/**
 * Calculate bounding box of a cubic Bezier curve
 * Taken from http://jsbin.com/ivomiq/56/edit (no credits available)
 * TODO: can we normalize this with the starting points set at 0 and then translated the bbox?
 * @param {number} begx starting point
 * @param {number} begy
 * @param {number} cp1x first control point
 * @param {number} cp1y
 * @param {number} cp2x second control point
 * @param {number} cp2y
 * @param {number} endx end of bezier
 * @param {number} endy
 * @return {TRectBounds} the rectangular bounds
 */
export function getBoundsOfCurve(
  begx: number,
  begy: number,
  cp1x: number,
  cp1y: number,
  cp2x: number,
  cp2y: number,
  endx: number,
  endy: number,
): TRectBounds {
  let argsString: string;
  if (config.cachesBoundsOfCurve) {
    // eslint-disable-next-line
    argsString = [...arguments].join();
    if (cache.boundsOfCurveCache[argsString]) {
      return cache.boundsOfCurveCache[argsString];
    }
  }

  const sqrt = Math.sqrt,
    abs = Math.abs,
    tvalues = [],
    bounds: [[x: number, y: number], [x: number, y: number]] = [
      [0, 0],
      [0, 0],
    ];

  let b = 6 * begx - 12 * cp1x + 6 * cp2x;
  let a = -3 * begx + 9 * cp1x - 9 * cp2x + 3 * endx;
  let c = 3 * cp1x - 3 * begx;

  for (let i = 0; i < 2; ++i) {
    if (i > 0) {
      b = 6 * begy - 12 * cp1y + 6 * cp2y;
      a = -3 * begy + 9 * cp1y - 9 * cp2y + 3 * endy;
      c = 3 * cp1y - 3 * begy;
    }

    if (abs(a) < 1e-12) {
      if (abs(b) < 1e-12) {
        continue;
      }
      const t = -c / b;
      if (0 < t && t < 1) {
        tvalues.push(t);
      }
      continue;
    }
    const b2ac = b * b - 4 * c * a;
    if (b2ac < 0) {
      continue;
    }
    const sqrtb2ac = sqrt(b2ac);
    const t1 = (-b + sqrtb2ac) / (2 * a);
    if (0 < t1 && t1 < 1) {
      tvalues.push(t1);
    }
    const t2 = (-b - sqrtb2ac) / (2 * a);
    if (0 < t2 && t2 < 1) {
      tvalues.push(t2);
    }
  }

  let j = tvalues.length;
  const jlen = j;
  const iterator = getPointOnCubicBezierIterator(
    begx,
    begy,
    cp1x,
    cp1y,
    cp2x,
    cp2y,
    endx,
    endy,
  );
  while (j--) {
    const { x, y } = iterator(tvalues[j]);
    bounds[0][j] = x;
    bounds[1][j] = y;
  }

  bounds[0][jlen] = begx;
  bounds[1][jlen] = begy;
  bounds[0][jlen + 1] = endx;
  bounds[1][jlen + 1] = endy;
  const result: TRectBounds = [
    new Point(Math.min(...bounds[0]), Math.min(...bounds[1])),
    new Point(Math.max(...bounds[0]), Math.max(...bounds[1])),
  ];
  if (config.cachesBoundsOfCurve) {
    cache.boundsOfCurveCache[argsString!] = result;
  }
  return result;
}

/**
 * Converts arc to a bunch of cubic Bezier curves
 * @param {number} fx starting point x
 * @param {number} fy starting point y
 * @param {TParsedArcCommand} coords Arc command
 */
export const fromArcToBeziers = (
  fx: number,
  fy: number,
  [_, rx, ry, rot, large, sweep, tx, ty]: TParsedArcCommand,
): TParsedAbsoluteCubicCurveCommand[] => {
  const segsNorm = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot);

  for (let i = 0, len = segsNorm.length; i < len; i++) {
    segsNorm[i][1] += fx;
    segsNorm[i][2] += fy;
    segsNorm[i][3] += fx;
    segsNorm[i][4] += fy;
    segsNorm[i][5] += fx;
    segsNorm[i][6] += fy;
  }
  return segsNorm;
};

/**
 * This function takes a parsed SVG path and makes it simpler for fabricJS logic.
 * Simplification consist of:
 * - All commands converted to absolute (lowercase to uppercase)
 * - S converted to C
 * - T converted to Q
 * - A converted to C
 * @param {TComplexPathData} path the array of commands of a parsed SVG path for `Path`
 * @return {TSimplePathData} the simplified array of commands of a parsed SVG path for `Path`
 * TODO: figure out how to remove the type assertions in a nice way
 */
export const makePathSimpler = (path: TComplexPathData): TSimplePathData => {
  // x and y represent the last point of the path, AKA the previous command point.
  // we add them to each relative command to make it an absolute comment.
  // we also swap the v V h H with L, because are easier to transform.
  let x = 0,
    y = 0;
  // x1 and y1 represent the last point of the subpath. the subpath is started with
  // m or M command. When a z or Z command is drawn, x and y need to be resetted to
  // the last x1 and y1.
  let x1 = 0,
    y1 = 0;
  // previous will host the letter of the previous command, to handle S and T.
  // controlX and controlY will host the previous reflected control point
  const destinationPath: TSimplePathData = [];
  let previous,
    // placeholders
    controlX = 0,
    controlY = 0;
  for (const parsedCommand of path) {
    const current: TComplexParsedCommand = [...parsedCommand];
    let converted: TSimpleParsedCommand | undefined;
    switch (
      current[0] // first letter
    ) {
      case 'l': // lineto, relative
        current[1] += x;
        current[2] += y;
      // falls through
      case 'L':
        x = current[1];
        y = current[2];
        converted = ['L', x, y];
        break;
      case 'h': // horizontal lineto, relative
        current[1] += x;
      // falls through
      case 'H':
        x = current[1];
        converted = ['L', x, y];
        break;
      case 'v': // vertical lineto, relative
        current[1] += y;
      // falls through
      case 'V':
        y = current[1];
        converted = ['L', x, y];
        break;
      case 'm': // moveTo, relative
        current[1] += x;
        current[2] += y;
      // falls through
      case 'M':
        x = current[1];
        y = current[2];
        x1 = current[1];
        y1 = current[2];
        converted = ['M', x, y];
        break;
      case 'c': // bezierCurveTo, relative
        current[1] += x;
        current[2] += y;
        current[3] += x;
        current[4] += y;
        current[5] += x;
        current[6] += y;
      // falls through
      case 'C':
        controlX = current[3];
        controlY = current[4];
        x = current[5];
        y = current[6];
        converted = ['C', current[1], current[2], controlX, controlY, x, y];
        break;
      case 's': // shorthand cubic bezierCurveTo, relative
        current[1] += x;
        current[2] += y;
        current[3] += x;
        current[4] += y;
      // falls through
      case 'S':
        // would be sScC but since we are swapping sSc for C, we check just that.
        if (previous === 'C') {
          // calculate reflection of previous control points
          controlX = 2 * x - controlX;
          controlY = 2 * y - controlY;
        } else {
          // If there is no previous command or if the previous command was not a C, c, S, or s,
          // the control point is coincident with the current point
          controlX = x;
          controlY = y;
        }
        x = current[3];
        y = current[4];
        converted = ['C', controlX, controlY, current[1], current[2], x, y];
        // converted[3] and converted[4] are NOW the second control point.
        // we keep it for the next reflection.
        controlX = converted[3];
        controlY = converted[4];
        break;
      case 'q': // quadraticCurveTo, relative
        current[1] += x;
        current[2] += y;
        current[3] += x;
        current[4] += y;
      // falls through
      case 'Q':
        controlX = current[1];
        controlY = current[2];
        x = current[3];
        y = current[4];
        converted = ['Q', controlX, controlY, x, y];
        break;
      case 't': // shorthand quadraticCurveTo, relative
        current[1] += x;
        current[2] += y;
      // falls through
      case 'T':
        if (previous === 'Q') {
          // calculate reflection of previous control point
          controlX = 2 * x - controlX;
          controlY = 2 * y - controlY;
        } else {
          // If there is no previous command or if the previous command was not a Q, q, T or t,
          // assume the control point is coincident with the current point
          controlX = x;
          controlY = y;
        }
        x = current[1];
        y = current[2];
        converted = ['Q', controlX, controlY, x, y];
        break;
      case 'a':
        current[6] += x;
        current[7] += y;
      // falls through
      case 'A':
        fromArcToBeziers(x, y, current).forEach((b) => destinationPath.push(b));
        x = current[6];
        y = current[7];
        break;
      case 'z':
      case 'Z':
        x = x1;
        y = y1;
        converted = ['Z'];
        break;
      default:
    }
    if (converted) {
      destinationPath.push(converted);
      previous = converted[0];
    } else {
      previous = '';
    }
  }
  return destinationPath;
};

// todo verify if we can just use the point class here
/**
 * Calc length from point x1,y1 to x2,y2
 * @param {number} x1 starting point x
 * @param {number} y1 starting point y
 * @param {number} x2 starting point x
 * @param {number} y2 starting point y
 * @return {number} length of segment
 */
const calcLineLength = (
  x1: number,
  y1: number,
  x2: number,
  y2: number,
): number => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);

/**
 * Get an iterator that takes a percentage and returns a point
 * @param {number} begx
 * @param {number} begy
 * @param {number} cp1x
 * @param {number} cp1y
 * @param {number} cp2x
 * @param {number} cp2y
 * @param {number} endx
 * @param {number} endy
 */
const getPointOnCubicBezierIterator =
  (
    begx: number,
    begy: number,
    cp1x: number,
    cp1y: number,
    cp2x: number,
    cp2y: number,
    endx: number,
    endy: number,
  ) =>
  (pct: number) => {
    const c1 = CB1(pct),
      c2 = CB2(pct),
      c3 = CB3(pct),
      c4 = CB4(pct);
    return new Point(
      endx * c1 + cp2x * c2 + cp1x * c3 + begx * c4,
      endy * c1 + cp2y * c2 + cp1y * c3 + begy * c4,
    );
  };

const QB1 = (t: number) => t ** 2;
const QB2 = (t: number) => 2 * t * (1 - t);
const QB3 = (t: number) => (1 - t) ** 2;

const getTangentCubicIterator =
  (
    p1x: number,
    p1y: number,
    p2x: number,
    p2y: number,
    p3x: number,
    p3y: number,
    p4x: number,
    p4y: number,
  ) =>
  (pct: number) => {
    const qb1 = QB1(pct),
      qb2 = QB2(pct),
      qb3 = QB3(pct),
      tangentX =
        3 * (qb3 * (p2x - p1x) + qb2 * (p3x - p2x) + qb1 * (p4x - p3x)),
      tangentY =
        3 * (qb3 * (p2y - p1y) + qb2 * (p3y - p2y) + qb1 * (p4y - p3y));
    return Math.atan2(tangentY, tangentX);
  };

const getPointOnQuadraticBezierIterator =
  (
    p1x: number,
    p1y: number,
    p2x: number,
    p2y: number,
    p3x: number,
    p3y: number,
  ) =>
  (pct: number) => {
    const c1 = QB1(pct),
      c2 = QB2(pct),
      c3 = QB3(pct);
    return new Point(
      p3x * c1 + p2x * c2 + p1x * c3,
      p3y * c1 + p2y * c2 + p1y * c3,
    );
  };

const getTangentQuadraticIterator =
  (
    p1x: number,
    p1y: number,
    p2x: number,
    p2y: number,
    p3x: number,
    p3y: number,
  ) =>
  (pct: number) => {
    const invT = 1 - pct,
      tangentX = 2 * (invT * (p2x - p1x) + pct * (p3x - p2x)),
      tangentY = 2 * (invT * (p2y - p1y) + pct * (p3y - p2y));
    return Math.atan2(tangentY, tangentX);
  };

// this will run over a path segment (a cubic or quadratic segment) and approximate it
// with 100 segments. This will good enough to calculate the length of the curve
const pathIterator = (
  iterator: (pct: number) => Point,
  x1: number,
  y1: number,
) => {
  let tempP = new Point(x1, y1),
    tmpLen = 0;
  for (let perc = 1; perc <= 100; perc += 1) {
    const p = iterator(perc / 100);
    tmpLen += calcLineLength(tempP.x, tempP.y, p.x, p.y);
    tempP = p;
  }
  return tmpLen;
};

/**
 * Given a pathInfo, and a distance in pixels, find the percentage from 0 to 1
 * that correspond to that pixels run over the path.
 * The percentage will be then used to find the correct point on the canvas for the path.
 * @param {Array} segInfo fabricJS collection of information on a parsed path
 * @param {number} distance from starting point, in pixels.
 * @return {TPointAngle} info object with x and y ( the point on canvas ) and angle, the tangent on that point;
 */
const findPercentageForDistance = (
  segInfo: TCurveInfo<'Q' | 'C'>,
  distance: number,
): TPointAngle => {
  let perc = 0,
    tmpLen = 0,
    tempP: XY = { x: segInfo.x, y: segInfo.y },
    p: XY = { ...tempP },
    nextLen: number,
    nextStep = 0.01,
    lastPerc = 0;
  // nextStep > 0.0001 covers 0.00015625 that 1/64th of 1/100
  // the path
  const iterator = segInfo.iterator,
    angleFinder = segInfo.angleFinder;
  while (tmpLen < distance && nextStep > 0.0001) {
    p = iterator(perc);
    lastPerc = perc;
    nextLen = calcLineLength(tempP.x, tempP.y, p.x, p.y);
    // compare tmpLen each cycle with distance, decide next perc to test.
    if (nextLen + tmpLen > distance) {
      // we discard this step and we make smaller steps.
      perc -= nextStep;
      nextStep /= 2;
    } else {
      tempP = p;
      perc += nextStep;
      tmpLen += nextLen;
    }
  }
  return { ...p, angle: angleFinder(lastPerc) };
};

/**
 * Run over a parsed and simplified path and extract some information (length of each command and starting point)
 * @param {TSimplePathData} path parsed path commands
 * @return {TPathSegmentInfo[]} path commands information
 */
export const getPathSegmentsInfo = (
  path: TSimplePathData,
): TPathSegmentInfo[] => {
  let totalLength = 0,
    //x2 and y2 are the coords of segment start
    //x1 and y1 are the coords of the current point
    x1 = 0,
    y1 = 0,
    x2 = 0,
    y2 = 0,
    iterator,
    tempInfo: TPathSegmentInfo;
  const info: TPathSegmentInfo[] = [];
  for (const current of path) {
    const basicInfo: TPathSegmentInfoCommon<keyof TPathSegmentCommandInfo> = {
      x: x1,
      y: y1,
      command: current[0],
      length: 0,
    };
    switch (
      current[0] //first letter
    ) {
      case 'M':
        tempInfo = <TPathSegmentInfoCommon<'M'>>basicInfo;
        tempInfo.x = x2 = x1 = current[1];
        tempInfo.y = y2 = y1 = current[2];
        break;
      case 'L':
        tempInfo = <TPathSegmentInfoCommon<'L'>>basicInfo;
        tempInfo.length = calcLineLength(x1, y1, current[1], current[2]);
        x1 = current[1];
        y1 = current[2];
        break;
      case 'C':
        iterator = getPointOnCubicBezierIterator(
          x1,
          y1,
          current[1],
          current[2],
          current[3],
          current[4],
          current[5],
          current[6],
        );
        tempInfo = <TCurveInfo<'C'>>basicInfo;
        tempInfo.iterator = iterator;
        tempInfo.angleFinder = getTangentCubicIterator(
          x1,
          y1,
          current[1],
          current[2],
          current[3],
          current[4],
          current[5],
          current[6],
        );
        tempInfo.length = pathIterator(iterator, x1, y1);

        x1 = current[5];
        y1 = current[6];
        break;
      case 'Q':
        iterator = getPointOnQuadraticBezierIterator(
          x1,
          y1,
          current[1],
          current[2],
          current[3],
          current[4],
        );
        tempInfo = <TCurveInfo<'Q'>>basicInfo;
        tempInfo.iterator = iterator;
        tempInfo.angleFinder = getTangentQuadraticIterator(
          x1,
          y1,
          current[1],
          current[2],
          current[3],
          current[4],
        );
        tempInfo.length = pathIterator(iterator, x1, y1);
        x1 = current[3];
        y1 = current[4];
        break;
      case 'Z':
        // we add those in order to ease calculations later
        tempInfo = <TEndPathInfo>basicInfo;
        tempInfo.destX = x2;
        tempInfo.destY = y2;
        tempInfo.length = calcLineLength(x1, y1, x2, y2);
        x1 = x2;
        y1 = y2;
        break;
    }
    totalLength += tempInfo.length;
    info.push(tempInfo);
  }
  info.push({ length: totalLength, x: x1, y: y1 });
  return info;
};

/**
 * Get the point on the path that is distance along the path
 * @param path
 * @param distance
 * @param infos
 */
export const getPointOnPath = (
  path: TSimplePathData,
  distance: number,
  infos: TPathSegmentInfo[] = getPathSegmentsInfo(path),
): TPointAngle | undefined => {
  let i = 0;
  while (distance - infos[i].length > 0 && i < infos.length - 2) {
    distance -= infos[i].length;
    i++;
  }
  const segInfo = infos[i],
    segPercent = distance / segInfo.length,
    segment = path[i];

  switch (segInfo.command) {
    case 'M':
      return { x: segInfo.x, y: segInfo.y, angle: 0 };
    case 'Z':
      return {
        ...new Point(segInfo.x, segInfo.y).lerp(
          new Point(segInfo.destX, segInfo.destY),
          segPercent,
        ),
        angle: Math.atan2(segInfo.destY - segInfo.y, segInfo.destX - segInfo.x),
      };
    case 'L':
      return {
        ...new Point(segInfo.x, segInfo.y).lerp(
          new Point(segment[1]!, segment[2]!),
          segPercent,
        ),
        angle: Math.atan2(segment[2]! - segInfo.y, segment[1]! - segInfo.x),
      };
    case 'C':
      return findPercentageForDistance(segInfo, distance);
    case 'Q':
      return findPercentageForDistance(segInfo, distance);
    default:
    // throw Error('Invalid command');
  }
};

const rePathCmdAll = new RegExp(rePathCommand, 'gi');
const regExpArcCommandPoints = new RegExp(reArcCommandPoints, 'g');
const reMyNum = new RegExp(reNum, 'gi');
const commandLengths = {
  m: 2,
  l: 2,
  h: 1,
  v: 1,
  c: 6,
  s: 4,
  q: 4,
  t: 2,
  a: 7,
} as const;
/**
 *
 * @param {string} pathString
 * @return {TComplexPathData} An array of SVG path commands
 * @example <caption>Usage</caption>
 * parsePath('M 3 4 Q 3 5 2 1 4 0 Q 9 12 2 1 4 0') === [
 *   ['M', 3, 4],
 *   ['Q', 3, 5, 2, 1, 4, 0],
 *   ['Q', 9, 12, 2, 1, 4, 0],
 * ];
 */
export const parsePath = (pathString: string): TComplexPathData => {
  const chain: TComplexPathData = [];
  const all = pathString.match(rePathCmdAll) ?? [];
  for (const matchStr of all) {
    // take match string and save the first letter as the command
    const commandLetter = matchStr[0] as TComplexParsedCommandType;
    // in case of Z we have very little to do
    if (commandLetter === 'z' || commandLetter === 'Z') {
      chain.push([commandLetter]);
      continue;
    }
    const commandLength =
      commandLengths[
        commandLetter.toLowerCase() as keyof typeof commandLengths
      ];

    let paramArr = [];
    if (commandLetter === 'a' || commandLetter === 'A') {
      // the arc command ha some peculariaties that requires a special regex other than numbers
      // it is possible to avoid using a space between the sweep and large arc flags, making them either
      // 00, 01, 10 or 11, making them identical to a plain number for the regex reMyNum
      // reset the regexp
      regExpArcCommandPoints.lastIndex = 0;
      for (let out = null; (out = regExpArcCommandPoints.exec(matchStr)); ) {
        paramArr.push(...out.slice(1));
      }
    } else {
      paramArr = matchStr.match(reMyNum) || [];
    }

    // inspect the length of paramArr, if is longer than commandLength
    // we are dealing with repeated commands
    for (let i = 0; i < paramArr.length; i += commandLength) {
      const newCommand = new Array(commandLength) as TComplexParsedCommand;
      const transformedCommand = repeatedCommands[commandLetter];
      newCommand[0] =
        i > 0 && transformedCommand ? transformedCommand : commandLetter;
      for (let j = 0; j < commandLength; j++) {
        newCommand[j + 1] = parseFloat(paramArr[i + j]);
      }
      chain.push(newCommand);
    }
  }
  return chain;
};

/**
 *
 * Converts points to a smooth SVG path
 * @param {XY[]} points Array of points
 * @param {number} [correction] Apply a correction to the path (usually we use `width / 1000`). If value is undefined 0 is used as the correction value.
 * @return {(string|number)[][]} An array of SVG path commands
 */
export const getSmoothPathFromPoints = (
  points: Point[],
  correction = 0,
): TSimplePathData => {
  let p1 = new Point(points[0]),
    p2 = new Point(points[1]),
    multSignX = 1,
    multSignY = 0;
  const path: TSimplePathData = [],
    len = points.length,
    manyPoints = len > 2;

  if (manyPoints) {
    multSignX = points[2].x < p2.x ? -1 : points[2].x === p2.x ? 0 : 1;
    multSignY = points[2].y < p2.y ? -1 : points[2].y === p2.y ? 0 : 1;
  }
  path.push([
    'M',
    p1.x - multSignX * correction,
    p1.y - multSignY * correction,
  ]);
  let i;
  for (i = 1; i < len; i++) {
    if (!p1.eq(p2)) {
      const midPoint = p1.midPointFrom(p2);
      // p1 is our bezier control point
      // midpoint is our endpoint
      // start point is p(i-1) value.
      path.push(['Q', p1.x, p1.y, midPoint.x, midPoint.y]);
    }
    p1 = points[i];
    if (i + 1 < points.length) {
      p2 = points[i + 1];
    }
  }
  if (manyPoints) {
    multSignX = p1.x > points[i - 2].x ? 1 : p1.x === points[i - 2].x ? 0 : -1;
    multSignY = p1.y > points[i - 2].y ? 1 : p1.y === points[i - 2].y ? 0 : -1;
  }
  path.push([
    'L',
    p1.x + multSignX * correction,
    p1.y + multSignY * correction,
  ]);
  return path;
};

/**
 * Transform a path by transforming each segment.
 * it has to be a simplified path or it won't work.
 * WARNING: this depends from pathOffset for correct operation
 * @param {TSimplePathData} path fabricJS parsed and simplified path commands
 * @param {TMat2D} transform matrix that represent the transformation
 * @param {Point} [pathOffset] `Path.pathOffset`
 * @returns {TSimplePathData} the transformed path
 */
export const transformPath = (
  path: TSimplePathData,
  transform: TMat2D,
  pathOffset: Point,
): TSimplePathData => {
  if (pathOffset) {
    transform = multiplyTransformMatrices(transform, [
      1,
      0,
      0,
      1,
      -pathOffset.x,
      -pathOffset.y,
    ]);
  }
  return path.map((pathSegment) => {
    const newSegment: TSimpleParsedCommand = [...pathSegment];
    for (let i = 1; i < pathSegment.length - 1; i += 2) {
      // TODO: is there a way to get around casting to any?
      const { x, y } = transformPoint(
        {
          x: pathSegment[i] as number,
          y: pathSegment[i + 1] as number,
        },
        transform,
      );
      newSegment[i] = x;
      newSegment[i + 1] = y;
    }
    return newSegment;
  });
};

/**
 * Returns an array of path commands to create a regular polygon
 * @param {number} numVertexes
 * @param {number} radius
 * @returns {TSimplePathData} An array of SVG path commands
 */
export const getRegularPolygonPath = (
  numVertexes: number,
  radius: number,
): TSimplePathData => {
  const interiorAngle = (Math.PI * 2) / numVertexes;
  // rotationAdjustment rotates the path by 1/2 the interior angle so that the polygon always has a flat side on the bottom
  // This isn't strictly necessary, but it's how we tend to think of and expect polygons to be drawn
  let rotationAdjustment = -halfPI;
  if (numVertexes % 2 === 0) {
    rotationAdjustment += interiorAngle / 2;
  }
  const d = new Array(numVertexes + 1);
  for (let i = 0; i < numVertexes; i++) {
    const rad = i * interiorAngle + rotationAdjustment;
    const { x, y } = new Point(cos(rad), sin(rad)).scalarMultiply(radius);
    d[i] = [i === 0 ? 'M' : 'L', x, y];
  }
  d[numVertexes] = ['Z'];
  return d;
};

/**
 * Join path commands to go back to svg format
 * @param {TSimplePathData} pathData fabricJS parsed path commands
 * @param {number} fractionDigits number of fraction digits to "leave"
 * @return {String} joined path 'M 0 0 L 20 30'
 */
export const joinPath = (pathData: TSimplePathData, fractionDigits?: number) =>
  pathData
    .map((segment) => {
      return segment
        .map((arg, i) => {
          if (i === 0) return arg;
          return fractionDigits === undefined
            ? arg
            : toFixed(arg, fractionDigits);
        })
        .join(' ');
    })
    .join(' ');
