import { useEffect, useId, useRef, useState } from "preact/hooks";

import V2 from "./V2";

const range = (n: number) => [...new Array(n)].map((_, i) => i);
type Nyp = 0 | 1 | 2 | 3;
type Edge = [V2, V2];

const INITIAL_DIRECTIONS: Record<Nyp, V2> = {
  0b00: new V2(+1, -1),
  0b01: new V2(-1, -1),
  0b10: new V2(-1, +1),
  0b11: new V2(+1, +1),
};

const edgesForPath = (path: V2[]) => {
  const edges = new Array<Edge>(Math.max(0, path.length - 1));
  for (let i = 0; i < path.length - 1; i++) edges[i] = [path[i], path[i + 1]];
  return edges;
};

function redraw(ctx: CanvasRenderingContext2D, str: string) {
  function computePath(stream: Nyp[]): { path: V2[]; consumed: number } {
    let pos = V2.zero;
    const path: V2[] = [pos];
    let consumed = 0;

    let edgesTouched = new Set<string>();
    const edgeToSetValue = (edge: Edge) => String([...edge].sort());

    const tryMove = (displacement: V2, distance: number = 1): boolean => {
      const intermediateEdges = range(distance).map(
        (offset): Edge => [
          pos.add(displacement.scale(offset)),
          pos.add(displacement.scale(offset + 1)),
        ],
      );

      if (
        intermediateEdges
          .map(edgeToSetValue)
          .some(edgeValue => edgesTouched.has(edgeValue))
      )
        return false;

      intermediateEdges.forEach(edge => edgesTouched.add(edgeToSetValue(edge)));
      pos = pos.add(displacement.scale(distance));
      path.push(pos);
      consumed++;
      return true;
    };

    if (!tryMove(INITIAL_DIRECTIONS[stream[0]])) {
      return { path, consumed };
    }

    let dir = pos;
    for (const nyp of stream.slice(1)) {
      const moveRight = nyp % 2 === 1;
      const moveBy = nyp >> 1 ? 2 : 1;
      dir = moveRight ? new V2(-dir.y, dir.x) : new V2(dir.y, -dir.x);
      if (!tryMove(dir, moveBy)) break;
    }
    return { path, consumed };
  }

  const utf8Bytes = [...new TextEncoder().encode(str)];
  const nyps = utf8Bytes.flatMap(byte =>
    [6, 4, 2, 0].map(power => (0b11 & (byte >> power)) as Nyp),
  );

  let consumedSum = 0;
  const paths = [];
  while (consumedSum < nyps.length) {
    const { path, consumed } = computePath(nyps.slice(consumedSum));
    paths.push(path);
    consumedSum += consumed;
  }

  const adjustPath = (path: V2[]) => {
    const minPoint = path.reduce(
      ([a, b], [x, y]) => new V2(Math.min(a, x), Math.min(b, y)),
      V2.zero,
    );
    path = path.map(vertex => vertex.sub(minPoint).scale(10));
    return {
      path,
      width: Math.max(...path.map(vertex => vertex.x)),
    };
  };

  ctx.reset();
  let offsetX = 0;
  for (const { path, width } of paths.map(adjustPath)) {
    const totalPathLength =
      edgesForPath(path)
        .map(([from, to]) => Math.abs(to.sub(from).x))
        .reduce((a, b) => a + b, 0) / 10;
    const totalSamples = totalPathLength * 100;

    ctx.strokeStyle = "#f00";
    ctx.beginPath();
    for (let i = 0; i <= totalSamples; i++) {
      const t = i / totalSamples;
      let pathRedux = path;
      while (pathRedux.length > 1) {
        pathRedux = edgesForPath(pathRedux).map(([from, to]) =>
          from.add(to.sub(from).scale(t)),
        );
      }
      const solePoint = pathRedux[0];
      ctx.lineTo(solePoint.x + offsetX, solePoint.y);
    }
    ctx.stroke();

    ctx.strokeStyle = "#00f7";
    ctx.beginPath();
    path.forEach(([x, y]) => ctx.lineTo(x + offsetX, y));
    ctx.stroke();

    offsetX += width + 10;
  }
}

export default function App({}: {}) {
  const inputId = useId();
  const canvasRef = useRef<HTMLCanvasElement>(null);

  const [input, setInput] = useState("");
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d")!;
    redraw(ctx, input);
  }, [canvasRef, input]);

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: "1em" }}>
      <div style={{ display: "flex", gap: "0.5em" }}>
        <label htmlFor={inputId}>Input: </label>
        <input
          id={inputId}
          style={{ flexGrow: 1 }}
          type="text"
          value={input}
          onInput={e => setInput((e.target as HTMLInputElement).value)}
        />
      </div>
      <div style={{ overflowX: "scroll" }}>
        <canvas ref={canvasRef} width={4000} height={200} />
      </div>
    </div>
  );
}
