import { interpolate, Extrapolation } from "react-native-reanimated";
import parseSVG from "parse-svg-path";
import absSVG from "abs-svg-path";
import normalizeSVG from "normalize-svg-path";

import type { Vector } from "./Vectors";
import { cartesian2Polar } from "./Coordinates";
import { cubicBezierYForX } from "./Math";

type SVGCloseCommand = ["Z"];
type SVGMoveCommand = ["M", number, number];
type SVGCurveCommand = ["C", number, number, number, number, number, number];
type SVGNormalizedCommands = [
  SVGMoveCommand,
  ...(SVGCurveCommand | SVGCloseCommand)[],
];

interface Curve {
  to: Vector;
  c1: Vector;
  c2: Vector;
}

export type Path = {
  move: Vector;
  curves: Curve[];
  close: boolean;
};

/**
 * @summary Serialize a path into an SVG path string
 * @worklet
 */
export const serialize = (path: Path) => {
  "worklet";
  return `M${path.move.x},${path.move.y} ${path.curves
    .map((c) => `C${c.c1.x},${c.c1.y} ${c.c2.x},${c.c2.y} ${c.to.x},${c.to.y}`)
    .join(" ")}${path.close ? "Z" : ""}`;
};

/**
 * @description ⚠️ this function cannot run on the UI thread. It must be executed on the JS thread
 * @summary Parse an SVG path into a sequence of Bèzier curves.
 * The SVG is normalized to have absolute values and to be approximated to a sequence of Bèzier curves.
 */
export const parse = (d: string): Path => {
  if (!d || d.trim() === "") {
    return createPath({ x: 0, y: 0 });
  }
  const segments: SVGNormalizedCommands = normalizeSVG(absSVG(parseSVG(d)));
  if (!segments || segments.length === 0) {
    return createPath({ x: 0, y: 0 });
  }
  const path = createPath({ x: segments[0][1], y: segments[0][2] });
  segments.forEach((segment) => {
    if (segment[0] === "Z") {
      close(path);
    } else if (segment[0] === "C") {
      addCurve(path, {
        c1: {
          x: segment[1],
          y: segment[2],
        },
        c2: {
          x: segment[3],
          y: segment[4],
        },
        to: {
          x: segment[5],
          y: segment[6],
        },
      });
    }
  });
  return path;
};

/**
 * @summary Interpolate between paths.
 * @worklet
 */
export const interpolatePath = (
  value: number,
  inputRange: number[],
  outputRange: Path[],
  extrapolate = Extrapolation.CLAMP
) => {
  "worklet";
  const path = {
    move: {
      x: interpolate(
        value,
        inputRange,
        outputRange.map((p) => p.move.x),
        extrapolate
      ),
      y: interpolate(
        value,
        inputRange,
        outputRange.map((p) => p.move.y),
        extrapolate
      ),
    },
    curves: outputRange[0].curves.map((_, index) => ({
      c1: {
        x: interpolate(
          value,
          inputRange,
          outputRange.map((p) => p.curves[index].c1.x),
          extrapolate
        ),
        y: interpolate(
          value,
          inputRange,
          outputRange.map((p) => p.curves[index].c1.y),
          extrapolate
        ),
      },
      c2: {
        x: interpolate(
          value,
          inputRange,
          outputRange.map((p) => p.curves[index].c2.x),
          extrapolate
        ),
        y: interpolate(
          value,
          inputRange,
          outputRange.map((p) => p.curves[index].c2.y),
          extrapolate
        ),
      },
      to: {
        x: interpolate(
          value,
          inputRange,
          outputRange.map((p) => p.curves[index].to.x),
          extrapolate
        ),
        y: interpolate(
          value,
          inputRange,
          outputRange.map((p) => p.curves[index].to.y),
          extrapolate
        ),
      },
    })),
    close: outputRange[0].close,
  };
  return serialize(path);
};

/**
 * @summary Interpolate two paths with an animation value that goes from 0 to 1
 * @worklet
 */
export const mixPath = (
  value: number,
  p1: Path,
  p2: Path,
  extrapolate = Extrapolation.CLAMP
) => {
  "worklet";
  return interpolatePath(value, [0, 1], [p1, p2], extrapolate);
};

/**
 * @summary Create a new path
 * @worklet
 */
export const createPath = (move: Vector): Path => {
  "worklet";
  return {
    move,
    curves: [],
    close: false,
  };
};

/**
 * @summary Add an arc command to a path
 * @worklet
 */
export const addArc = (path: Path, corner: Vector, to: Vector) => {
  "worklet";
  const last = path.curves[path.curves.length - 1];
  const from = last ? last.to : path.move;
  const arc = 9 / 16;
  path.curves.push({
    c1: {
      x: (corner.x - from.x) * arc + from.x,
      y: (corner.y - from.y) * arc + from.y,
    },
    c2: {
      x: (corner.x - to.x) * arc + to.x,
      y: (corner.y - to.y) * arc + to.y,
    },
    to,
  });
};

/**
 * @summary Add a cubic Bèzier curve command to a path.
 * @worklet
 */
export const addCurve = (path: Path, c: Curve) => {
  "worklet";
  path.curves.push({
    c1: c.c1,
    c2: c.c2,
    to: c.to,
  });
};

/**
 * @summary Add a line command to a path.
 * @worklet
 */
export const addLine = (path: Path, to: Vector) => {
  "worklet";
  const last = path.curves[path.curves.length - 1];
  const from = last ? last.to : path.move;
  path.curves.push({
    c1: from,
    c2: to,
    to,
  });
};

/**
 * @summary Add a quadratic Bèzier curve command to a path.
 * @worklet
 */
export const addQuadraticCurve = (path: Path, cp: Vector, to: Vector) => {
  "worklet";
  const last = path.curves[path.curves.length - 1];
  const from = last ? last.to : path.move;
  path.curves.push({
    c1: {
      x: from.x / 3 + (2 / 3) * cp.x,
      y: from.y / 3 + (2 / 3) * cp.y,
    },
    c2: {
      x: to.x / 3 + (2 / 3) * cp.x,
      y: to.y / 3 + (2 / 3) * cp.y,
    },
    to,
  });
};

/**
 * @summary Add a close command to a path.
 * @worklet
 */
export const close = (path: Path) => {
  "worklet";
  path.close = true;
};

interface SelectedCurve {
  from: Vector;
  curve: Curve;
}

interface NullableSelectedCurve {
  from: Vector;
  curve: Curve | null;
}

/**
 * @worklet
 */
const curveIsFound = (c: NullableSelectedCurve): c is SelectedCurve => {
  "worklet";
  return c.curve !== null;
};

/**
 * @summary Return the curves at x. This function assumes that only one curve is available at x
 * @worklet
 */
export const selectCurve = (path: Path, x: number): SelectedCurve | null => {
  "worklet";
  const result: NullableSelectedCurve = {
    from: path.move,
    curve: null,
  };
  for (let i = 0; i < path.curves.length; i++) {
    const c = path.curves[i];
    const contains =
      result.from.x > c.to.x
        ? x >= c.to.x && x <= result.from.x
        : x >= result.from.x && x <= c.to.x;
    if (contains) {
      result.curve = c;
      break;
    }
    result.from = c.to;
  }
  if (!curveIsFound(result)) {
    return null;
  }
  return result;
};

/**
 * @summary Return the y value of a path given its x coordinate
 * @example
    const p1 = parse(
      "M150,0 C150,0 0,75 200,75 C75,200 200,225 200,225 C225,200 200,150 0,150"
    );
    // 75
    getYForX(p1, 200))
    // ~151
    getYForX(p1, 50)
 * @worklet
 */
export const getYForX = (path: Path, x: number, precision = 2) => {
  "worklet";
  const c = selectCurve(path, x);
  if (c === null) {
    return null;
  }
  return cubicBezierYForX(
    x,
    c.from,
    c.curve.c1,
    c.curve.c2,
    c.curve.to,
    precision
  );
};

const controlPoint = (
  current: Vector,
  previous: Vector,
  next: Vector,
  reverse: boolean,
  smoothing: number
) => {
  "worklet";
  const p = previous || current;
  const n = next || current;
  // Properties of the opposed-line
  const lengthX = n.x - p.x;
  const lengthY = n.y - p.y;

  const o = cartesian2Polar({ x: lengthX, y: lengthY });
  // If is end-control-point, add PI to the angle to go backward
  const angle = o.theta + (reverse ? Math.PI : 0);
  const length = o.radius * smoothing;
  // The control point position is relative to the current point
  const x = current.x + Math.cos(angle) * length;
  const y = current.y + Math.sin(angle) * length;
  return { x, y };
};

const exhaustiveCheck = (a: never): never => {
  throw new Error(`Unexhaustive handling for ${a}`);
};

/**
 * @summary Link points via a smooth cubic Bézier curves
 * from https://github.com/rainbow-me/rainbow
 * @worklet
 */
export const curveLines = (
  points: Vector<number>[],
  smoothing: number,
  strategy: "complex" | "bezier" | "simple"
) => {
  "worklet";
  const path = createPath(points[0]);
  // build the d attributes by looping over the points
  for (let i = 0; i < points.length; i++) {
    if (i === 0) {
      continue;
    }
    const point = points[i];
    const next = points[i + 1];
    const prev = points[i - 1];
    const cps = controlPoint(prev, points[i - 2], point, false, smoothing);
    const cpe = controlPoint(point, prev, next, true, smoothing);
    switch (strategy) {
      case "simple":
        const cp = {
          x: (cps.x + cpe.x) / 2,
          y: (cps.y + cpe.y) / 2,
        };
        addQuadraticCurve(path, cp, point);
        break;
      case "bezier":
        const p0 = points[i - 2] || prev;
        const p1 = points[i - 1];
        const cp1x = (2 * p0.x + p1.x) / 3;
        const cp1y = (2 * p0.y + p1.y) / 3;
        const cp2x = (p0.x + 2 * p1.x) / 3;
        const cp2y = (p0.y + 2 * p1.y) / 3;
        const cp3x = (p0.x + 4 * p1.x + point.x) / 6;
        const cp3y = (p0.y + 4 * p1.y + point.y) / 6;
        path.curves.push({
          c1: { x: cp1x, y: cp1y },
          c2: { x: cp2x, y: cp2y },
          to: { x: cp3x, y: cp3y },
        });
        if (i === points.length - 1) {
          path.curves.push({
            to: points[points.length - 1],
            c1: points[points.length - 1],
            c2: points[points.length - 1],
          });
        }
        break;
      case "complex":
        path.curves.push({
          to: point,
          c1: cps,
          c2: cpe,
        });
        break;
      default:
        exhaustiveCheck(strategy);
    }
  }
  return path;
};
