import {
  Actor,
  type Actor as RuntimeActor,
} from "../scene/Actor"
import { Collider, type Collider as CollisionShape } from "./Collider"
import {
  CollisionWorld,
  type CollisionWorld as Collision,
} from "./CollisionWorld"
import type { Terrain } from "./Terrain"
import {
  RenderScene,
  type Billboard,
  type RenderInstance,
  type RenderPrototype,
  type RenderScene as Scene,
} from "../render/RenderScene"
import type { Chunk } from "../render/Chunk"
import type { DrawGroup } from "../render/Material"
import type { SkyConfig } from "../render/Sky"
import type { Mat4 } from "../math/Mat4"
import type { Vec3 } from "../math/Vec3"

export type LevelDefinition<World> = {
  terrain: Terrain
  actorWorld: World
  actors: readonly RuntimeActor<World>[]
  staticColliders: CollisionShape[]
  staticGroups: DrawGroup[]
  chunks: Chunk[]
  billboards: Billboard[]
  sky: SkyConfig
}

/** Live engine world. Behavior-bearing actors stay here on the main thread; only
 * `render` is clone-safe and sent to workers. */
export type Level<World> = {
  readonly terrain: Terrain
  readonly actorWorld: World
  readonly actors: readonly RuntimeActor<World>[]
  readonly collision: Collision
  readonly render: Scene
}

const prototypeIndexes = new WeakMap<object, ReadonlyMap<object, number>>()

export namespace Level {
  export function create<World>(definition: LevelDefinition<World>): Level<World> {
    const actors = Object.freeze([...definition.actors])
    const prototypes: RenderPrototype[] = []
    const prototypeIndex = new Map<object, number>()
    for (const actor of actors) {
      if (!prototypeIndex.has(actor.definition)) {
        prototypeIndex.set(actor.definition, prototypes.length)
        prototypes.push(actor.prototype)
      }
    }
    const level: Level<World> = {
      terrain: definition.terrain,
      actorWorld: definition.actorWorld,
      actors,
      collision: CollisionWorld.create(
        definition.terrain,
        definition.staticColliders,
      ),
      render: Object.freeze({
        chunks: Object.freeze([...definition.chunks]),
        staticGroups: Object.freeze([...definition.staticGroups]),
        billboards: Object.freeze([...definition.billboards]),
        prototypes: Object.freeze(prototypes),
        maxInstances: actors.length,
        sky: definition.sky,
      }),
    }
    prototypeIndexes.set(level, prototypeIndex)
    return level
  }

  export function update<World>(level: Level<World>, dt: number): void {
    for (const actor of level.actors) {
      Actor.update(actor, dt, level.actorWorld)
    }
  }

  export function visibleInstances<World>(
    level: Level<World>,
    viewProjection: Mat4,
  ): RenderInstance[] {
    const prototypeIndex = prototypeIndexes.get(level)
    if (prototypeIndex === undefined) {
      throw new Error("level was not created by Level.create")
    }
    const instances: RenderInstance[] = []
    for (const actor of level.actors) {
      const prototype = prototypeIndex.get(actor.definition)
      if (prototype !== undefined) {
        instances.push({ prototype, ...Actor.transform(actor) })
      }
    }
    return RenderScene.visibleInstances(level.render, instances, viewProjection)
  }

  export function refreshActorColliders<World>(
    level: Level<World>,
    focus: Vec3,
    range: number,
  ): void {
    const dynamic = level.collision.dynamicColliders
    dynamic.length = 0
    const rangeSquared = range * range
    for (const actor of level.actors) {
      const collider = Actor.collider(actor, level.actorWorld)
      if (collider === null) {
        continue
      }
      const dx = Collider.centerX(collider) - focus.x
      const dz = Collider.centerZ(collider) - focus.z
      if (dx * dx + dz * dz <= rangeSquared) {
        dynamic.push(collider)
      }
    }
  }
}
