import {
  BufferGeometry,
  Float32BufferAttribute,
  Mesh,
  Vector2,
  Vector3,
} from 'three';
import { QuadtreeMesher } from './quadtree-mesher';

export enum LODQuadrant {
  TOP_LEFT,
  TOP_RIGHT,
  BOTTOM_RIGHT,
  BOTTOM_LEFT,
}

export enum LODSide {
  TOP,
  RIGHT,
  BOTTOM,
  LEFT,
}

export function mirrorSide(side: number): number {
  return (side + 2) % 4;
}

export function isAdjacent(side: number, quadrant: number): boolean {
  return (4 + quadrant - side) % 4 <= 1;
}

export function reflectSide(side: number, quadrant: number): number {
  return side % 2 ? (quadrant % 2 ? quadrant - 1 : quadrant + 1) : 3 - quadrant;
}

export class QuadtreeNode {
  private _children: QuadtreeNode[] = [];
  private _neighbors: QuadtreeNode[] = [];
  private _detailDifferences: number[] = [];
  private _leaf = true;
  private _mesh?: Mesh;

  constructor(
    public root: QuadtreeMesher,
    public parent: QuadtreeNode,
    public level: number,
    public quadrant: number,
    public position: Vector2,
  ) {}

  public dispose() {
    if (this._mesh) {
      this._destroyMesh();
    }
    this._children.forEach((child) => child.dispose());
    this._children.length = 0;
  }

  public isMeshed() {
    return !!this._mesh;
  }

  public update(camera: Vector3) {
    if (this.root.actionsLeft === 0) {
      return;
    }

    const size = this.root.size / Math.pow(2, this.level);
    const abs = new Vector3(
      this.position.x + size / 2,
      camera.y,
      this.position.y + size / 2,
    );

    if (this._leaf) {
      if (this._canSubdivide() && abs.distanceTo(camera) < size) {
        this._subdivide();
        this.root.actionsLeft -= 1;
        return;
      }

      if (!this._mesh) {
        this._createMesh();
        this.root.actionsLeft -= 1;
        return;
      }
    } else if (!this._leaf) {
      if (abs.distanceTo(camera) > size) {
        this._merge();
        this.root.actionsLeft -= 1;
        return;
      }

      if (
        this.isMeshed() &&
        this._children.every((child) => child.isMeshed())
      ) {
        this._destroyMesh();
      }

      this._children.forEach((child) => child.update(camera));
    }
  }

  public setNeighbor(side: LODSide, neighbor: QuadtreeNode) {
    this._neighbors[side] = neighbor;
    if (neighbor) {
      const mirrored = mirrorSide(side);
      neighbor._neighbors[mirrored] = this;
      neighbor._updateNeighborDetailDifferences(mirrored);
    }
    this._updateNeighborDetailDifferences(side);
  }

  public findNeighbor(side: LODSide) {
    if (!this._neighbors[side] && this.parent && this.parent._neighbors[side]) {
      const neighbor =
        this.parent._neighbors[side]?._children[
          reflectSide(side, this.quadrant)
        ];
      if (neighbor) {
        this.setNeighbor(side, neighbor);
      } else {
        return;
      }
    }

    if (!this._leaf) {
      for (let i = 0; i < 4; i++) {
        if (isAdjacent(side, i)) {
          this._children[i].findNeighbor(side);
        }
      }
    }
  }

  private _equalOrHigherNeighbor(side: LODSide): QuadtreeNode {
    for (let node: QuadtreeNode = this; !!node; node = node.parent) {
      if (node?._neighbors[side]) {
        return node._neighbors[side];
      }
    }
    return null;
  }

  private _updateNeighborDetailDifferences(side: LODSide) {
    for (let i = 0; i < 4; i++) {
      const neighbor = this._equalOrHigherNeighbor(i);
      if (neighbor) {
        this._detailDifferences[i] = Math.min(this.level - neighbor.level, 4);
      }
    }

    if (!this._leaf) {
      for (let i = 0; i < 4; i++) {
        if (isAdjacent(side, i))
          this._children[i]._updateNeighborDetailDifferences(side);
      }
    }
  }

  private _createGeometry(): BufferGeometry {
    const apparentSize = this.root.size / Math.pow(2, this.level);
    const vertCount =
      this.root.size / Math.pow(2, this.root.maxDepth - this.level);
    const divisionLevel = Math.pow(2, this.level);
    const geometry = new BufferGeometry();
    const vertices = [];
    const normals = [];
    const indices = [];
    const uvs = [];

    for (let x = 0; x < vertCount; x++) {
      for (let y = 0; y < vertCount; y++) {
        const vertDivj = y / (vertCount - 1);
        const vertDivi = x / (vertCount - 1);

        const absX = this.position.x + vertDivj * apparentSize;
        const absY = this.position.y + vertDivi * apparentSize;
        const normal = this.root.getNormal(absX, absY);
        const height = this.root.getHeight(absX, absY);

        // Calculate relative resolution
        const pj = vertDivj * (this.root.size / divisionLevel);
        const pi = vertDivi * (this.root.size / divisionLevel);

        vertices.push(pj, height, pi);
        normals.push(normal.x, normal.y, normal.z);
        uvs.push(absX / (this.root.size - 1), absY / (this.root.size - 1));

        // Create quad
        if (x !== vertCount - 1 && y !== vertCount - 1) {
          const topLeft = x * vertCount + y;
          const topRight = topLeft + 1;
          const bottomLeft = (x + 1) * vertCount + y;
          const bottomRight = bottomLeft + 1;
          indices.push(
            topLeft,
            bottomLeft,
            topRight,
            topRight,
            bottomLeft,
            bottomRight,
          );
        }
      }
    }

    // for (let test = 1; test < vertCount; test += 2) {
    //   // bottom
    //   vertices[test * 3 + 1] = vertices[test * 3 + 1] + 2;
    //   // top
    //   const testi = vertCount * vertCount * 3 - vertCount * 3 + test * 3;
    //   vertices[testi + 1] = vertices[testi + 1] + 2;
    //   // right
    //   const testi2 = vertCount * 3 * test;
    //   vertices[testi2 + 1] = vertices[testi2 + 1] + 2;
    //   // left
    //   const testi3 = vertCount * 3 * test + (vertCount - 1) * 3;
    //   vertices[testi3 + 1] = vertices[testi3 + 1] + 2;
    // }

    geometry.setIndex(indices);
    geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3));
    geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3));
    geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2));

    return geometry;
  }

  private _merge(): boolean {
    if (this._leaf) {
      return false;
    }

    this._children.forEach((child) => child.dispose());
    this._children.length = 0;
    this._leaf = true;
    return true;
  }

  private _subdivide(): boolean {
    if (!this._canSubdivide()) {
      return false;
    }

    const level = this.level + 1;
    const stepLeft = this.root.size / Math.pow(2, level);
    const stepForward = this.root.size / Math.pow(2, level);

    const { x, y } = this.position;

    // Create children
    this._children[LODQuadrant.TOP_LEFT] = new QuadtreeNode(
      this.root,
      this,
      level,
      LODQuadrant.TOP_LEFT,
      new Vector2(x, y),
    );
    this._children[LODQuadrant.TOP_RIGHT] = new QuadtreeNode(
      this.root,
      this,
      level,
      LODQuadrant.TOP_RIGHT,
      new Vector2(stepLeft + x, y),
    );
    this._children[LODQuadrant.BOTTOM_RIGHT] = new QuadtreeNode(
      this.root,
      this,
      level,
      LODQuadrant.BOTTOM_RIGHT,
      new Vector2(stepLeft + x, stepForward + y),
    );
    this._children[LODQuadrant.BOTTOM_LEFT] = new QuadtreeNode(
      this.root,
      this,
      level,
      LODQuadrant.BOTTOM_LEFT,
      new Vector2(x, stepForward + y),
    );

    // Set sibling neighbors
    this._children[LODQuadrant.TOP_LEFT].setNeighbor(
      LODSide.RIGHT,
      this._children[LODQuadrant.TOP_RIGHT],
    );
    this._children[LODQuadrant.TOP_RIGHT].setNeighbor(
      LODSide.BOTTOM,
      this._children[LODQuadrant.BOTTOM_RIGHT],
    );
    this._children[LODQuadrant.BOTTOM_RIGHT].setNeighbor(
      LODSide.LEFT,
      this._children[LODQuadrant.BOTTOM_LEFT],
    );
    this._children[LODQuadrant.BOTTOM_LEFT].setNeighbor(
      LODSide.TOP,
      this._children[LODQuadrant.TOP_LEFT],
    );

    // set adjacent neighbors
    for (let i = 0; i < 4; i++) {
      if (this._neighbors[i] && !this._neighbors[i]._leaf) {
        this._neighbors[i].findNeighbor(mirrorSide(i));
      }
    }

    this._leaf = false;
    return true;
  }

  private _createMesh() {
    if (this._mesh) {
      this._destroyMesh();
    }

    const geometry = this._createGeometry();
    const mesh = new Mesh(geometry, this.root.material);
    mesh.position.set(this.position.x, 0, this.position.y);
    this.root.container.add(mesh);
    this._mesh = mesh;
  }

  private _destroyMesh() {
    this.root.container.remove(this._mesh);
    this._mesh = null;
  }

  private _canSubdivide() {
    return this._leaf && this.level < this.root.maxDepth - 1;
  }
}
