import { Mat4 as Matrix, type Mat4 } from "../math/Mat4"
import type { Vec2 } from "../math/Vec2"
import type { Vec3 } from "../math/Vec3"
import type { Camera } from "../scene/Camera"
import { Sprite } from "../scene/Sprite"
import { Framebuffer, type Framebuffer as Frame } from "./Framebuffer"
import { Frustum } from "./Frustum"
import type { DrawGroup, Material } from "./Material"
import { Rasterizer } from "./Rasterizer"
import type { RenderConfig } from "./RenderConfig"
import { Sky, type SkyConfig } from "./Sky"
import { Chunk, type Chunk as RenderChunk } from "./Chunk"

export type Billboard = {
  position: Vec3
  size: Vec2
  material: Material
}

export type RenderPrototype = {
  readonly groups: readonly DrawGroup[]
  readonly radius: number
  readonly minY: number
  readonly maxY: number
}

export type RenderTransform = {
  x: number
  y: number
  z: number
  heading: number
  scale: number
}

export type RenderInstance = RenderTransform & {
  prototype: number
}

/** Clone-safe render projection of a live level. Contains no behavior callbacks or
 * game-content registries, so workers need only engine code. */
export type RenderScene = {
  readonly chunks: readonly RenderChunk[]
  readonly staticGroups: readonly DrawGroup[]
  readonly billboards: readonly Billboard[]
  readonly prototypes: readonly RenderPrototype[]
  readonly maxInstances: number
  readonly sky: SkyConfig
}

export namespace RenderScene {
  export function visibleChunks(
    scene: RenderScene,
    viewProjection: Mat4,
  ): number[] {
    const frustum = Frustum.fromViewProj(viewProjection)
    const visible: number[] = []
    for (let i = 0; i < scene.chunks.length; i++) {
      const chunk = scene.chunks[i]
      if (
        Frustum.intersectsAabb(
          frustum,
          chunk.minX,
          chunk.minY,
          chunk.minZ,
          chunk.maxX,
          chunk.maxY,
          chunk.maxZ,
        )
      ) {
        visible.push(i)
      }
    }
    return visible
  }

  export function visibleInstances(
    scene: RenderScene,
    instances: RenderInstance[],
    viewProjection: Mat4,
  ): RenderInstance[] {
    const frustum = Frustum.fromViewProj(viewProjection)
    const visible: RenderInstance[] = []
    for (const instance of instances) {
      const prototype = scene.prototypes[instance.prototype]
      if (prototype === undefined) {
        continue
      }
      const radius = prototype.radius * instance.scale
      if (
        Frustum.intersectsAabb(
          frustum,
          instance.x - radius,
          instance.y + prototype.minY * instance.scale,
          instance.z - radius,
          instance.x + radius,
          instance.y + prototype.maxY * instance.scale,
          instance.z + radius,
        )
      ) {
        visible.push(instance)
      }
    }
    return visible
  }

  export function renderBand(
    framebuffer: Frame,
    scene: RenderScene,
    camera: Camera,
    viewProjection: Mat4,
    visible: number[],
    instances: RenderInstance[],
    config: RenderConfig,
    skyStep: number,
    time: number,
    y0: number,
    y1: number,
  ): void {
    Sky.render(framebuffer, camera, scene.sky, time, skyStep, y0, y1)
    drawGroups(framebuffer, scene.staticGroups, viewProjection, config, y0, y1)
    for (const index of visible) {
      const chunk = scene.chunks[index]
      const groups = Chunk.isFar(chunk, camera.position, config.lodDistance)
        ? chunk.far
        : chunk.near
      drawGroups(framebuffer, groups, viewProjection, config, y0, y1)
    }
    for (const billboard of scene.billboards) {
      const sprite = {
        position: billboard.position,
        size: billboard.size,
        texture: billboard.material.texture,
      }
      Rasterizer.draw(
        framebuffer,
        Sprite.billboard(sprite, camera),
        billboard.material.texture,
        viewProjection,
        config,
        billboard.material.cull,
        y0,
        y1,
      )
    }
    for (const instance of instances) {
      const prototype = scene.prototypes[instance.prototype]
      if (prototype === undefined) {
        continue
      }
      const modelViewProjection = Matrix.multiply(
        viewProjection,
        Matrix.compose(
          instance.x,
          instance.y,
          instance.z,
          instance.heading,
          instance.scale,
        ),
      )
      drawGroups(
        framebuffer,
        prototype.groups,
        modelViewProjection,
        config,
        y0,
        y1,
      )
    }
    Framebuffer.quantize(framebuffer, config, y0, y1)
  }

  export function triangleCount(
    scene: RenderScene,
    visible: number[],
    instances: RenderInstance[],
    eye: Vec3,
    lodDistance: number,
  ): number {
    let indices = groupIndices(scene.staticGroups) + scene.billboards.length * 6
    for (const index of visible) {
      const chunk = scene.chunks[index]
      indices += groupIndices(
        Chunk.isFar(chunk, eye, lodDistance) ? chunk.far : chunk.near,
      )
    }
    for (const instance of instances) {
      const prototype = scene.prototypes[instance.prototype]
      if (prototype !== undefined) {
        indices += groupIndices(prototype.groups)
      }
    }
    return (indices / 3) | 0
  }

  function drawGroups(
    framebuffer: Frame,
    groups: readonly DrawGroup[],
    matrix: Mat4,
    config: RenderConfig,
    y0: number,
    y1: number,
  ): void {
    for (const group of groups) {
      Rasterizer.draw(
        framebuffer,
        group.mesh,
        group.material.texture,
        matrix,
        config,
        group.material.cull,
        y0,
        y1,
      )
    }
  }

  function groupIndices(groups: readonly DrawGroup[]): number {
    let count = 0
    for (const group of groups) {
      count += group.mesh.indices.length
    }
    return count
  }
}
