import { canvas2d } from "./lib/html";
import { Prng, mulberry32 } from "./lib/prng";

const BLOWUP = 4 * 5;
const WIDTH = 240 / 5;
const HEIGHT = 135 / 5;

type Lookup2d = (x: number, y: number) => number;
function dim(width: number, height: number): Lookup2d {
  return function xy(x: number, y: number) {
    return (
      (((x % width) + width) % width) +
      width * (((y % height) + height) % height)
    );
  };
}

class IslandGrid {
  data: number[];
  rng: Prng;

  xy: Lookup2d;

  constructor(public width: number, public height: number, seed: number) {
    this.data = Array(width * height).fill(0);
    this.rng = mulberry32(seed);
    this.xy = dim(width, height);
  }

  public get(x: number, y: number): number {
    return this.data[this.xy(x, y)];
  }

  public set(x: number, y: number, tile: number) {
    this.data[this.xy(x, y)] = tile;
    console.log(x, y, this.xy(x, y), this.data[this.xy(x, y)]);
  }

  public floodSearch(
    startPos: number,
    shouldExpand: (tile: number) => boolean
  ): number[] {
    const len = this.data.length;
    const width = this.width;
    const seen = new Uint8Array(len);

    const hitPositions: number[] = [];

    function enqueue(pos: number) {
      if (!seen[pos]) {
        hitPositions.push(pos);
        seen[pos] = 1;
      }
    }

    enqueue(startPos);

    for (let i = 0; i < hitPositions.length; i++) {
      const pos = hitPositions[i];
      if (shouldExpand(this.data[pos])) {
        enqueue((pos - width) % len);
        enqueue((pos - 1) % len);
        enqueue((pos + 1) % len);
        enqueue((pos + width) % len);
      }
    }

    return hitPositions;
  }
}

function renderIslands(islands: IslandGrid, cx: CanvasRenderingContext2D) {
  for (let y = 0; y < islands.height; y++) {
    for (let x = 0; x < islands.width; x++) {
      const tile = islands.data[islands.xy(x, y)];
      switch (tile) {
        case 0:
          cx.fillStyle = "blue";
          break;
        case 1:
          cx.fillStyle = "yellow";
          break;
        case 2:
          cx.fillStyle = "#00ff00";
          break;
        case 3:
          cx.fillStyle = "#008800";
          break;
        case 4:
        case 5:
        case 6:
        case 7:
        case 8:
          cx.fillStyle = "#666666";
          break;
        default:
          cx.fillStyle = "#88aaff";
          break;
      }
      cx.fillRect(x, y, 1, 1);
    }
  }
}

export function IslandApplet() {
  const [canvas, cx] = canvas2d({
    width: WIDTH * BLOWUP,
    height: HEIGHT * BLOWUP,
  });
  cx.scale(BLOWUP, BLOWUP);

  const islands = new IslandGrid(WIDTH, HEIGHT, 128);

  const len = islands.data.length;
  const basePos = len >> 1;
  const width = islands.width;

  let timerId: number;

  function drop(pos: number) {
    const lowerNeighbors: number[] = [];

    function check(adjPos: number) {
      if (islands.data[adjPos] < islands.data[pos]) {
        lowerNeighbors.push(adjPos);
      }
    }

    // try to roll in cardinal directions

    check((pos - width) % len);
    check((pos - 1) % len);
    check((pos + 1) % len);
    check((pos + width) % len);

    if (lowerNeighbors.length > 0) {
      const downhill = lowerNeighbors[islands.rng() % lowerNeighbors.length];
      return drop(downhill);
    }

    // try to roll in diagonal directions

    check((pos - width - 1) % len);
    check((pos - width + 1) % len);
    check((pos + width - 1) % len);
    check((pos + width + 1) % len);

    if (lowerNeighbors.length > 0) {
      const downhill = lowerNeighbors[islands.rng() % lowerNeighbors.length];
      return drop(downhill);
    }

    // flat, increase elevation
    const newValue = ++islands.data[pos];
    if (newValue == 9) {
      clearInterval(timerId);
    }
  }

  function tick() {
    const islandTiles = islands.floodSearch(basePos, (tile) => tile > 0);

    drop(islandTiles[islands.rng() % islandTiles.length]);
    drop(islandTiles[islands.rng() % islandTiles.length]);
    drop(basePos - 8);

    const erodePos = islandTiles[islands.rng() % islandTiles.length];
    islands.data[erodePos] = Math.max(islands.data[erodePos] - 1, 0);

    renderIslands(islands, cx);
  }

  tick();

  timerId = setInterval(tick, 1000 / 30);

  return [canvas];
}

(globalThis as any).IslandApplet = IslandApplet;