type PointType = { x: number; y: number };

type LineFunc = (
  (pointA: PointType, pointB: PointType) => { length: number; angle: number}
);

type ControlPointClosure = (
  (current: PointType, previous: PointType, next: PointType, reverse?: boolean) => number[]
);

type ControlPointFunc = (
  (lineCale: LineFunc, smooth: number) => ControlPointClosure
);

type BezierCommandClosure = (
  (point: PointType, i: number, a: PointType[]) => string
);

type BezierCommandFunc = (
  (controlPoint: ControlPointClosure) => BezierCommandClosure
);

type SvgPathFunc = (
  (points: PointType[], command: BezierCommandClosure) => string
);

export const line: LineFunc = (pointA, pointB) => {
  const lengthX = pointB.x - pointA.x;
  const lengthY = pointB.y - pointA.y;

  return {
    length: Math.sqrt((lengthX ** 2) + (lengthY ** 2)),
    angle: Math.atan2(lengthY, lengthX),
  };
};

export const controlPoint: ControlPointFunc = (
  lineCale, smooth,
) => (
  current, previous, next, reverse,
): number[] => {
  // When 'current' is the first or last point of the array
  // 'previous' or 'next' don't exist.
  // Replace with 'current'
  const p = previous || current;
  const n = next || current;
  // Properties of the opposed-line
  const o = lineCale(p, n);
  // If is end-control-point, add PI to the angle to go backward
  const angle = o.angle + (reverse ? Math.PI : 0);
  const length = o.length * smooth;
  // 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];
};

export const bezierCommand: BezierCommandFunc = controlPointCalc => (point, i, a): string => {
  // start control point
  const [cpsX, cpsY] = controlPointCalc(a[i - 1], a[i - 2], point);
  // end control point
  const [cpeX, cpeY] = controlPointCalc(point, a[i - 1], a[i + 1], true);

  return `C ${cpsX},${cpsY} ${cpeX},${cpeY} ${point.x},${point.y}`;
};

export const svgPath: SvgPathFunc = (points, command) => {
  const d = points.reduce(
    (acc, point, i, a) => (i === 0 ? (
      `M ${point.x},${point.y}`
    ) : (
      `${acc} ${command(point, i, a)}`
    )),
    '',
  );
  return d;
};

export default line;
