import { Object3D, Vector2, Vector3 } from 'three';
import { WorldChunk } from '../../../common/world/world-chunk';
import { WorldManager } from '../../../common/world/world-manager';
import { ClientWorldChunkShader } from './client-world-chunk-shader';
import { ClientWorldTexture } from './client-world-texture';
import { QuadtreeMesher } from './quadtree/quadtree-mesher';

// TODO: distance loading

const BASE = '/assets/terrain/';

export class ClientWorld extends WorldManager {
  public world = new Object3D();
  private _chunkMeshers: QuadtreeMesher[] = [];
  private _chunkMeshQueue: WorldChunk[] = [];
  private _worldTextures: Map<string, ClientWorldTexture> = new Map();
  private _shader = new ClientWorldChunkShader(this._worldTextures);

  getNormalVector(x: number, y: number): Vector3 {
    const heightL = this.getPointHeight(x - 1, y);
    const heightR = this.getPointHeight(x + 1, y);
    const heightD = this.getPointHeight(x, y - 1);
    const heightU = this.getPointHeight(x, y + 1);
    const normalized = new Vector3(heightL - heightR, 2, heightD - heightU);
    normalized.normalize();
    return normalized;
  }

  async initialize() {
    await this.loadWorld();

    const noiseFile = `${BASE}/texture/${this.manifest.textureBombingNoise}`;

    await this.loadTextureList([
      noiseFile,
      ...this._chunks.map(
        (chunk) => `${BASE}/region/splat-${chunk.x}-${chunk.y}.png`,
      ),
      ...this.gatherManifestTextures(),
    ]);

    this._shader.initialize();
    this._shader.setNoise(this._worldTextures.get(noiseFile));
    this.createMeshes();
  }

  async loadTexture(src: string): Promise<ClientWorldTexture> {
    if (this._worldTextures.has(src)) {
      return this._worldTextures.get(src);
    }

    const tex = await ClientWorldTexture.loadTexture(src);
    this._worldTextures.set(src, tex);
    return tex;
  }

  async loadTextureList(srcList: string[]): Promise<void> {
    for (const src of srcList) {
      await this.loadTexture(src);
    }
  }

  update(camera: Vector3) {
    if (this._chunkMeshQueue.length) {
      const chunk = this._chunkMeshQueue.shift();
      const material = this._shader.getShader(
        chunk,
        `${BASE}/region/splat-${chunk.x}-${chunk.y}.png`,
        chunk.region.splat.map((file) => `${BASE}/texture/${file}`),
      );

      const root = new QuadtreeMesher(
        chunk.size,
        new Vector2(chunk.size * chunk.x, chunk.size * chunk.y),
        material,
      );

      root.getHeight = this.getHeight.bind(this);
      root.getNormal = this.getNormalVector.bind(this);
      root.initialize();

      this._chunkMeshers.push(root);
      this.world.add(root.container);
      return;
    }

    this._chunkMeshers.forEach((item) => item.update(camera));
  }

  private createMeshes() {
    this._chunks.forEach((chunk) => {
      this._chunkMeshQueue.push(chunk);
    });
  }

  private gatherManifestTextures(): string[] {
    return this.manifest.regionMap.reduce<string[]>((list, entry) => {
      const paths = entry.splat.map((file) => `${BASE}/texture/${file}`);
      return [...list, ...paths.filter((path) => !list.includes(path))];
    }, []);
  }
}
