import Quaternion from "../../../../core/geom/Quaternion.js";
import Vector2 from "../../../../core/geom/Vector2.js";
import Vector3 from "../../../../core/geom/Vector3.js";
import { DEG_TO_RAD } from "../../../../core/math/DEG_TO_RAD.js";
import { GameAssetType } from "../../../asset/GameAssetType.js";
import { GLTFAssetLoader } from "../../../asset/loaders/GLTFAssetLoader.js";
import Entity from "../../../ecs/Entity.js";
import GUIElementSystem from "../../../ecs/gui/GUIElementSystem.js";
import ViewportPositionSystem from "../../../ecs/gui/position/ViewportPositionSystem.js";
import { TransformAttachmentSystem } from "../../../ecs/transform-attachment/TransformAttachmentSystem.js";
import { Transform } from "../../../ecs/transform/Transform.js";
import { EngineHarness } from "../../../EngineHarness.js";
import { BehaviorComponent } from "../../../intelligence/behavior/ecs/BehaviorComponent.js";
import { BehaviorSystem } from "../../../intelligence/behavior/ecs/BehaviorSystem.js";
import { RotationBehavior } from "../../../intelligence/behavior/util/RotationBehavior.js";
import { SGMesh } from "../../ecs/mesh-v2/aggregate/SGMesh.js";
import { SGMeshSystem } from "../../ecs/mesh-v2/aggregate/SGMeshSystem.js";
import { ShadedGeometry } from "../../ecs/mesh-v2/ShadedGeometry.js";
import { ShadedGeometryFlags } from "../../ecs/mesh-v2/ShadedGeometryFlags.js";
import { ShadedGeometrySystem } from "../../ecs/mesh-v2/ShadedGeometrySystem.js";
import { ImpostorCaptureType } from "../octahedral/ImpostorCaptureType.js";
import { load_mesh_for_bake } from "../octahedral/util/load_mesh_for_bake.js";
import { VoxelImpostorShaderDepthV0 } from "./shader/VoxelImpostorShaderDepthV0.js";
import { VoxelImpostorShaderLitV0 } from "./shader/VoxelImpostorShaderLitV0.js";
import { VoxelImpostorShaderNormalsV0 } from "./shader/VoxelImpostorShaderNormalsV0.js";
import { VoxelImpostorShaderV0 } from "./shader/VoxelImpostorShaderV0.js";
import { VoxelImpostorShaderViewportDepthV0 } from "./shader/VoxelImpostorShaderViewportDepthV0.js";
import { make_voxel_impostor_geometry } from "./util/make_voxel_impostor_geometry.js";
import { VoxelImpostorBaker } from "./VoxelImpostorBaker.js";


/**
 * Prototype harness for the voxel impostor system. Mirrors the
 * structure of the octahedral system's prototypeBaker.js — load a GLTF
 * mesh, bake it through {@link VoxelImpostorBaker}, then lay out a
 * lineup of variants side-by-side so visual differences are easy to
 * compare against the truth mesh.
 *
 * Slot layout (along +X from x=10 to x=25):
 *   0  : V0 unlit (square splats)
 *   1  : V0 unlit with disc mask (shows splat shape clearly)
 *   2  : Lit V0 — only one that receives shadows
 *   3  : Normals viz (debug pair for the lit variant)
 *   4  : Viewport-depth viz
 *   5  : True mesh (ground-truth reference)
 *
 * @param {Engine} engine
 * @return {Promise<void>}
 */
async function main(engine) {
    await EngineHarness.buildBasics({
        engine,
        enableWater: false,
        cameraFieldOfView: 45,
        showFps: false,
        enableLights: true,
        enableShadows: true,
        cameraAutoClip: true,
        // Mirror the octahedral prototype's terrain layout so the same
        // EngineHarness defaults give us a sensible camera framing.
        terrainSize: new Vector2(18, 12),
        focus: new Vector3(17, 0, 10),
    });

    const ecd = engine.entityManager.dataset;

    const baker = new VoxelImpostorBaker();
    const renderer = engine.graphics.renderer;
    baker.renderer = renderer;

    // const path = 'data/models/LowPolyTownshipSet/Small_house/Small_house.gltf';
    // const path = 'data/models/samples/textured_unit_cube.gltf';
    // const path = 'data/models/samples/teapot.gltf';
    // const path = 'data/models/LowPolyTownshipSet/Barrel/model.gltf';
    // const path = 'data/models/LowPolyTownshipSet/Town_Hall/model.gltf';
    // const path = 'data/models/MOBA and Tower Defense/Tree_01.gltf';
    const path = 'data/models/samples/just_a_girl/scene.gltf';
    // const path = 'data/models/road_bike/road_bike.gltf'; //large CAD-type model

    const objects = await load_mesh_for_bake(engine, path);

    const id = baker.bake({
        objects,
        point_budget: 4096,
        // Atlas frame/resolution are conservative defaults — they
        // control how dense the SAMPLE cloud is, not the OUTPUT point
        // count. The budget is what caps the output. Tweak `frames`
        // up for hard-to-cover shapes (think: deep concavities) and
        // `resolution` up if voxels look noisy in their averaged colour.
        frames: 16,
        resolution: 1024,
        capture_type: ImpostorCaptureType.FullSphere
    });

    console.log(id);

    const t0 = Transform.fromJSON({
        position: { x: 10, y: 0.5, z: 10 },
        scale: 3,
        rotation: Quaternion.fromEulerAngles(0, 90 * DEG_TO_RAD, 0)
    });

    // Match the octahedral prototype's sizing: 3 world units across the
    // bounding sphere diameter. The voxel point cloud occupies the
    // same volume as the source mesh (each splat is sized in world
    // space — see the shaders' uPointDiameter doc — and the bounding
    // sphere comes straight from the bake), so the same Transform
    // scale that fits the octahedral card here also fits the voxel
    // cloud.
    t0.scale.setScalar(3 / (id.sphere_radius * 2));

    // Splat diameter in OBJECT space, auto-tuned by the baker —
    // sqrt(2) * voxel_size, which covers worst-case oblique
    // projections of each voxel cell. The shaders apply a render-time
    // inward-shift of half this value along the surface normal, so
    // even with the overlap the silhouette stays tight against the
    // original mesh outline. Override here for tighter (= voxel_size)
    // or looser (~ 1.5+ * voxel_size) fits per asset.
    const point_diameter_OS = id.point_diameter;

    // Slot 0: V0 unlit, square splats.
    make_voxel_entity({ id, t: t0, point_diameter: point_diameter_OS }).build(ecd);

    // Slot 1: V0 unlit with disc mask — shows splat shape directly,
    // useful for seeing the underlying point distribution.
    const t_disc = new Transform();
    t_disc.copy(t0);
    t_disc.position._add(3, 0, 0);
    make_voxel_entity({ id, t: t_disc, disc_mask: true, point_diameter: point_diameter_OS }).build(ecd);

    // Slot 2: lit variant — only one that receives shadows.
    const t_lit = new Transform();
    t_lit.copy(t0);
    t_lit.position._add(6, 0, 0);
    make_voxel_lit_entity({ id, t: t_lit, point_diameter: point_diameter_OS }).build(ecd);

    // Slot 3: world-space-normal viz — diagnostics for the lit shader.
    const t_normals = new Transform();
    t_normals.copy(t0);
    t_normals.position._add(9, 0, 0);
    make_voxel_normals_entity({ id, t: t_normals, point_diameter: point_diameter_OS }).build(ecd);

    // Slot 4: viewport-depth viz — diagnostics for the depth caster.
    const t_depth = new Transform();
    t_depth.copy(t0);
    t_depth.position._add(12, 0, 0);
    make_voxel_viewport_depth_entity({ id, t: t_depth, point_diameter: point_diameter_OS }).build(ecd);

    // Slot 5: true mesh ground truth.
    const t_truth = new Transform();
    t_truth.copy(t0);
    t_truth.position._add(15, 0, 0);

    const sg_mesh = SGMesh.fromURL(path);
    sg_mesh.castShadow = true;
    sg_mesh.receiveShadow = true;

    new Entity()
        .add(sg_mesh)
        .add(t_truth)
        .build(ecd);

    // [entity_impostor].forEach(e => make_spin(e));
}


/**
 * Apply a rotation behavior to an entity — useful for visually
 * confirming the impostor stays consistent under rotation. Off by
 * default, see the line at the end of main().
 *
 * @param {Entity} entity
 * @param {number} [speed]
 * @param {Vector3} [axis]
 */
function make_spin(entity, speed = 1, axis = Vector3.up) {
    /**
     * @type {Transform}
     */
    const t = entity.getComponent(Transform);

    if (t === null) {
        return;
    }

    entity.add(BehaviorComponent.from(RotationBehavior.fromJSON({
        axis, speed
    })));
}


/**
 * Build a paired depth material whose point diameter and disc mask
 * agree with the colour material. ShadedGeometry uses depth_material
 * for the shadow pass, so this keeps the cast silhouette aligned
 * with the visible one.
 *
 * @param {number} point_diameter   object-space splat diameter
 * @param {boolean} disc_mask
 */
function make_voxel_depth_material(point_diameter, disc_mask) {
    const m = new VoxelImpostorShaderDepthV0();
    m.uniforms.uPointDiameter.value = point_diameter;
    m.uniforms.uDiscMask.value = disc_mask;
    return m;
}


/**
 * Slot 0/1 factory: unlit voxel splats. Each instance is a
 * world-space-sized camera-facing billboard; the depth-caster
 * mirrors the same world size so the shadow silhouette agrees.
 *
 * @param {object} params
 * @param {VoxelImpostorDescription} params.id
 * @param {Transform} [params.t]
 * @param {boolean} [params.disc_mask]
 * @param {number} params.point_diameter   object-space splat diameter
 * @returns {Entity}
 */
function make_voxel_entity({ id, t = new Transform(), disc_mask = false, point_diameter }) {
    const mat = new VoxelImpostorShaderV0();
    mat.uniforms.uPointDiameter.value = point_diameter;
    mat.uniforms.uDiscMask.value = disc_mask;

    const transform = new Transform();
    transform.copy(t);

    const sg = ShadedGeometry.from(make_voxel_impostor_geometry(id), mat);
    sg.depth_material = make_voxel_depth_material(point_diameter, disc_mask);
    // Unlit splats don't receive shadow, same convention as the
    // octahedral V0 (its lit shader is the only receiver).
    sg.clearFlag(ShadedGeometryFlags.ReceiveShadow);

    return new Entity()
        .add(transform)
        .add(sg);
}


/**
 * Slot 2 factory: lit voxel splats. Casts AND receives shadows. The
 * lighting equations consume the per-instance baked normal directly,
 * so each splat's shading follows the surface orientation at the
 * sampled voxel.
 *
 * @param {object} params
 * @param {VoxelImpostorDescription} params.id
 * @param {Transform} [params.t]
 * @param {number} params.point_diameter
 * @returns {Entity}
 */
function make_voxel_lit_entity({ id, t = new Transform(), point_diameter }) {
    const mat = new VoxelImpostorShaderLitV0();
    mat.uniforms.uPointDiameter.value = point_diameter;

    const transform = new Transform();
    transform.copy(t);

    const sg = ShadedGeometry.from(make_voxel_impostor_geometry(id), mat);
    sg.depth_material = make_voxel_depth_material(point_diameter, false);
    // CastShadow + ReceiveShadow on by default.

    return new Entity()
        .add(transform)
        .add(sg);
}


/**
 * Slot 3 factory: per-voxel normal as RGB. Reads the same iNormal
 * attribute the lit shader feeds its lighting equations.
 *
 * @param {object} params
 * @param {VoxelImpostorDescription} params.id
 * @param {Transform} [params.t]
 * @param {number} params.point_diameter
 * @returns {Entity}
 */
function make_voxel_normals_entity({ id, t = new Transform(), point_diameter }) {
    const mat = new VoxelImpostorShaderNormalsV0();
    mat.uniforms.uPointDiameter.value = point_diameter;

    const transform = new Transform();
    transform.copy(t);

    const sg = ShadedGeometry.from(make_voxel_impostor_geometry(id), mat);
    sg.depth_material = make_voxel_depth_material(point_diameter, false);
    // Viz pass: don't participate in shadow casting/receiving, same as
    // the octahedral normals viz — keeps it from influencing neighbours.
    sg.clearFlag(ShadedGeometryFlags.CastShadow);
    sg.clearFlag(ShadedGeometryFlags.ReceiveShadow);

    return new Entity()
        .add(transform)
        .add(sg);
}


/**
 * Slot 4 factory: viewport-depth viz. Helpful for verifying the splat
 * cloud's depth matches the truth mesh (eg before bumping into a
 * receiving wall).
 *
 * @param {object} params
 * @param {VoxelImpostorDescription} params.id
 * @param {Transform} [params.t]
 * @param {number} params.point_diameter
 * @returns {Entity}
 */
function make_voxel_viewport_depth_entity({ id, t = new Transform(), point_diameter }) {
    const mat = new VoxelImpostorShaderViewportDepthV0();
    mat.uniforms.uPointDiameter.value = point_diameter;

    const transform = new Transform();
    transform.copy(t);

    const sg = ShadedGeometry.from(make_voxel_impostor_geometry(id), mat);
    sg.depth_material = make_voxel_depth_material(point_diameter, false);
    sg.clearFlag(ShadedGeometryFlags.CastShadow);
    sg.clearFlag(ShadedGeometryFlags.ReceiveShadow);

    return new Entity()
        .add(transform)
        .add(sg);
}


new EngineHarness().initialize({
    configuration(config, engine) {
        config.addSystem(new GUIElementSystem(engine.gui.view, engine));
        config.addSystem(new ViewportPositionSystem(engine.graphics.viewport.size));
        config.addSystem(new ShadedGeometrySystem(engine));
        config.addSystem(new SGMeshSystem(engine));
        config.addSystem(new TransformAttachmentSystem());
        config.addSystem(new BehaviorSystem(engine));

        const gltfAssetLoader = new GLTFAssetLoader();

        config.addLoader(GameAssetType.ModelGLTF, gltfAssetLoader);
        config.addLoader(GameAssetType.ModelGLTF_JSON, gltfAssetLoader);
    }
}).then(main);
