import { Prng, UINT_MAX, mulberry32 } from "../lib/prng";
import { BEACH, ICECAP } from "./data";
import {
  ALL_ISLANDS,
  BIG_ISLANDS,
  GREEN_ISLANDS,
  LobeGenerator,
  NO_ISLAND,
  ROCKY_ISLANDS,
  SMALL_ISLANDS,
} from "./generators";

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

  generators: LobeGenerator[] = [];

  done = false;

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

    const islandBag = this.shuffle([
      this.choose(BIG_ISLANDS),
      this.choose(ROCKY_ISLANDS),
      this.choose(GREEN_ISLANDS),
      this.choose(SMALL_ISLANDS),
      this.choose(SMALL_ISLANDS),
      this.choose(ALL_ISLANDS),
      this.choose(ALL_ISLANDS),
      NO_ISLAND,
      NO_ISLAND,
      NO_ISLAND,
      NO_ISLAND,
      NO_ISLAND,
      NO_ISLAND,
    ]);

    const islandCount = islandBag.length;
    const spacing = (Math.PI * 2) / islandCount;
    const rootX = width / 2;
    const rootY = height / 2;
    const xScale = width / 4;
    const yScale = height / 4;
    for (let i = 0; i < islandCount; i++) {
      const rScale = (this.rng() / UINT_MAX) * 2 - 1;
      const y = rootY + Math.sin(spacing * i) * yScale * rScale;
      const x = rootX + Math.cos(spacing * i) * xScale * rScale;

      this.generators.push(islandBag[i](this, this.xy(x | 0, y | 0)));
    }
  }

  public xy(x: number, y: number): number {
    return (
      (((x % this.width) + this.width) % this.width) +
      this.width * (((y % this.height) + this.height) % this.height)
    );
  }

  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;
  }

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

    const check = (adjPos: number) => {
      if (this.data[adjPos] < this.data[pos]) {
        lowerNeighbors.push(adjPos);
      }
    };

    // try to roll in cardinal directions

    check((pos - this.width) % this.data.length);
    check((pos - 1) % this.data.length);
    check((pos + 1) % this.data.length);
    check((pos + this.width) % this.data.length);

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

    // try to roll in diagonal directions

    check((pos - this.width - 1) % this.data.length);
    check((pos - this.width + 1) % this.data.length);
    check((pos + this.width - 1) % this.data.length);
    check((pos + this.width + 1) % this.data.length);

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

    // flat, increase elevation
    const newValue = ++this.data[pos];
    if (newValue == ICECAP) {
      this.done = true;
    }
  }

  public choose<T>(list: T[]) {
    if (list.length == 0) {
      throw new Error("Picking from empty list");
    }
    return list[this.rng() % list.length];
  }

  public shuffle<T>(list: T[]) {
    const shuffled = list.slice();
    for (let i = 0; i < shuffled.length - 1; i++) {
      const swapIndex = (this.rng() % (shuffled.length - i)) + i;
      [shuffled[i], shuffled[swapIndex]] = [shuffled[swapIndex], shuffled[i]];
    }
    return shuffled;
  }

  public dropWithin(tiles: number[]) {
    if (tiles.length > 0) {
      this.drop(this.choose(tiles));
    }
  }

  public step() {
    this.done = this.generators
      .map((generator) => generator())
      .every((done) => done);
  }
}