import { Prng, UINT_MAX, mulberry32 } from "../lib/prng";
import { WATER } from "./data";
import {
  ALL_ISLANDS,
  BIG_ISLANDS,
  GREEN_ISLANDS,
  LobeGenerator,
  NO_ISLAND,
  ROCKY_ISLANDS,
  SMALL_ISLANDS,
  VOIDS,
} 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(WATER);
    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),
      this.choose(VOIDS),
      this.choose(VOIDS),
      this.choose(VOIDS),
      this.choose(VOIDS),
      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

    this.forCardinals(pos, check);

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

    // try to roll in diagonal directions

    this.forDiagonals(pos, check);

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

    // flat, increase elevation
    ++this.data[pos];
  }

  public sinkhole(pos: number): void {
    const higherNeighbors: number[] = [];

    // try to pull from neighbors

    this.forNeighbors(pos, (adjPos: number) => {
      if (this.data[adjPos] > this.data[pos]) {
        higherNeighbors.push(adjPos);
      }
    });

    if (higherNeighbors.length > 0) {
      const uphill = higherNeighbors[this.rng() % higherNeighbors.length];
      return this.sinkhole(uphill);
    }

    // flat, decrease elevation
    this.data[pos] = Math.max(this.data[pos] - 1, -3);
  }

  public forCardinals(pos: number, action: (adjPos: number) => void) {
    action((pos - this.width) % this.data.length);
    action((pos - 1) % this.data.length);
    action((pos + 1) % this.data.length);
    action((pos + this.width) % this.data.length);
  }
  public forDiagonals(pos: number, action: (adjPos: number) => void) {
    action((pos - this.width - 1) % this.data.length);
    action((pos - this.width + 1) % this.data.length);
    action((pos + this.width - 1) % this.data.length);
    action((pos + this.width + 1) % this.data.length);
  }
  public forNeighbors(pos: number, action: (adjPos: number) => void) {
    this.forCardinals(pos, action);
    this.forDiagonals(pos, action);
  }

  public deepenWater() {
    for (let i = 0; i < this.data.length; i++) {
      if (this.data[i] == WATER) {
        let isShore = false;
        this.forNeighbors(i, (adjPos) => {
          if (this.data[adjPos] > WATER) {
            isShore = true;
          }
        });
        if (!isShore) {
          this.data[i] = WATER - 1;
        }
      }
    }
  }

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