import { STRIDE, type Mesh } from "../scene/Mesh"
import type { Chunk, Bounds3 } from "./Chunk"
import type { DrawGroup, Material } from "./Material"

export type MeshBatch = {
  mesh: (material: Material) => Mesh
  use: (material: Material, mesh: Mesh) => void
}

export type ChunkItem = {
  position: { x: number; z: number }
  bakeNear: (batch: MeshBatch) => void
  bakeFar?: (batch: MeshBatch) => void
}

export type ChunkCell = {
  x0: number
  z0: number
  x1: number
  z1: number
}

export type ChunkBuilder = {
  minX: number
  minZ: number
  maxX: number
  maxZ: number
  columns: number
  rows: number
  bakeCell: (near: MeshBatch, far: MeshBatch, cell: ChunkCell) => void
}

export namespace ChunkBuilder {
  export function build(config: ChunkBuilder, items: ChunkItem[]): Chunk[] {
    const width = (config.maxX - config.minX) / config.columns
    const depth = (config.maxZ - config.minZ) / config.rows
    const chunks: Chunk[] = []
    for (let column = 0; column < config.columns; column++) {
      const x0 = config.minX + column * width
      const x1 = x0 + width
      for (let row = 0; row < config.rows; row++) {
        const z0 = config.minZ + row * depth
        const z1 = z0 + depth
        const nearMeshes = new Map<Material, Mesh>()
        const farMeshes = new Map<Material, Mesh>()
        const near = batch(nearMeshes)
        const far = batch(farMeshes)
        config.bakeCell(near, far, { x0, z0, x1, z1 })
        for (const item of items) {
          if (inCell(item.position, x0, z0, x1, z1)) {
            item.bakeNear(near)
            item.bakeFar?.(far)
          }
        }
        const nearGroups = groups(nearMeshes)
        const farGroups = groups(farMeshes)
        const box = bounds([...nearMeshes.values(), ...farMeshes.values()])
        if (box !== null) {
          chunks.push({ ...box, near: nearGroups, far: farGroups })
        }
      }
    }
    return chunks
  }

  function batch(meshes: Map<Material, Mesh>): MeshBatch {
    return {
      mesh(material) {
        let mesh = meshes.get(material)
        if (mesh === undefined) {
          mesh = { verts: [], indices: [] }
          meshes.set(material, mesh)
        }
        return mesh
      },
      use(material, mesh) {
        meshes.set(material, mesh)
      },
    }
  }

  function groups(meshes: Map<Material, Mesh>): DrawGroup[] {
    const result: DrawGroup[] = []
    for (const [material, mesh] of meshes) {
      if (mesh.indices.length > 0) {
        result.push({ mesh, material })
      }
    }
    return result
  }

  function inCell(
    position: { x: number; z: number },
    x0: number,
    z0: number,
    x1: number,
    z1: number,
  ): boolean {
    return (
      position.x >= x0 && position.x < x1 && position.z >= z0 && position.z < z1
    )
  }

  function bounds(meshes: Mesh[]): Bounds3 | null {
    let minX = Infinity
    let minY = Infinity
    let minZ = Infinity
    let maxX = -Infinity
    let maxY = -Infinity
    let maxZ = -Infinity
    for (const mesh of meshes) {
      for (let i = 0; i < mesh.verts.length; i += STRIDE) {
        minX = Math.min(minX, mesh.verts[i])
        minY = Math.min(minY, mesh.verts[i + 1])
        minZ = Math.min(minZ, mesh.verts[i + 2])
        maxX = Math.max(maxX, mesh.verts[i])
        maxY = Math.max(maxY, mesh.verts[i + 1])
        maxZ = Math.max(maxZ, mesh.verts[i + 2])
      }
    }
    return maxX < minX ? null : { minX, minY, minZ, maxX, maxY, maxZ }
  }
}
