import { GUI } from "dat.gui";
import {
    ClampToEdgeWrapping,
    DataTexture,
    LinearFilter,
    Matrix4,
    MeshBasicMaterial,
    MeshStandardMaterial,
    NearestFilter,
    PlaneBufferGeometry,
    RepeatWrapping,
    RGBAFormat,
    TextureLoader,
    UnsignedByteType
} from "three";
import '../../../../../../../css/game.scss';
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
import { array_copy } from "../../../core/collection/array/array_copy.js";
import { is_typed_array_equals } from "../../../core/collection/array/typed/is_typed_array_equals.js";
import { Color } from "../../../core/color/Color.js";
import { kelvin_to_rgb } from "../../../core/color/kelvin/kelvin_to_rgb.js";
import { AABB3 } from "../../../core/geom/3d/aabb/AABB3.js";
import { compose_matrix4_array } from "../../../core/geom/3d/mat4/compose_matrix4_array.js";
import { visualize_tetrahedral_mesh } from "../../../core/geom/3d/tetrahedra/visualize_tetrahedral_mesh.js";
import Quaternion from "../../../core/geom/Quaternion.js";
import { v3_length } from "../../../core/geom/vec3/v3_length.js";
import Vector3 from "../../../core/geom/Vector3.js";
import { max2 } from "../../../core/math/max2.js";
import { min2 } from "../../../core/math/min2.js";
import { RAD_TO_DEG } from "../../../core/math/RAD_TO_DEG.js";
import { randomFloatBetween } from "../../../core/math/random/randomFloatBetween.js";
import { seededRandom } from "../../../core/math/random/seededRandom.js";
import { delay } from "../../../core/process/delay.js";
import ButtonView from "../../../view/elements/button/ButtonView.js";
import { GLTFAssetLoader } from "../../asset/loaders/GLTFAssetLoader.js";
import Entity from "../../ecs/Entity.js";
import GUIElement from "../../ecs/gui/GUIElement.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 { Camera } from "../ecs/camera/Camera.js";
import { focal_length_to_fov } from "../ecs/camera/focal_length_to_fov.js";
import TopDownCameraController from "../ecs/camera/topdown/TopDownCameraController.js";
import { Light } from "../ecs/light/Light.js";
import LightSystem from "../ecs/light/LightSystem.js";
import { LightType } from "../ecs/light/LightType.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 { three_object_to_entity_composition } from "../ecs/mesh-v2/three_object_to_entity_composition.js";
import { GizmoRenderingPlugin } from "../render/gizmo/GizmoRenderingPlugin.js";
import { MaterialTransformer, ProbeResolutionStage } from "./gi/material/MaterialTransformer.js";
import { lpv_volume_bake_via_task } from "./lpv/build_probes_for_scene.js";
import { OctahedralDepthDebuggerWidget } from "./lpv/depth/octahedral/OctahedralDepthDebuggerWidget.js";
import { VLPDepthMapVisualisation } from "./lpv/depth/octahedral/v2/VLPDepthMapVisualisation.js";
import { LightProbeVolume } from "./lpv/LightProbeVolume.js";
import { lpv_obtain_storage_cached_volume } from "./lpv/lpv_obtain_storage_cached_volume.js";
import { lpv_visualize_probes } from "./lpv/util/lpv_visualise_probes.js";
import { lpv_build_editor } from "./lpv_build_editor.js";

/**
 *
 * @param {Storage} storage
 * @param {*} component
 * @param {BinaryClassSerializationAdapter} adapter
 * @param {string} key
 * @param {Signal[]} signals
 */
async function persist_component(storage, component, adapter, key = 'temp', signals) {


    const current_state = new BinaryBuffer();

    const full_key = `persisted_transform:${key}`;

    if (await storage.promiseContains(full_key)) {

        const loaded = await storage.promiseLoadBinary(full_key);

        const buffer = BinaryBuffer.fromArrayBuffer(loaded);

        adapter.deserialize(buffer, component);
    }

    // make a binary state copy
    adapter.serialize(current_state, component);


    for (let i = 0; i < signals.length; i++) {
        signals[i].add(store);
    }

    function store() {

        const buffer = new BinaryBuffer();

        adapter.serialize(buffer, component);

        buffer.trim();


        if (!is_typed_array_equals(current_state.__data_uint8, buffer.__data_uint8)) {
            // change detected

            storage.promiseStoreBinary(full_key, buffer.data);


            current_state.setCapacity(buffer.length);
            array_copy(buffer.__data_uint8, 0, current_state.__data_uint8, 0, buffer.capacity);
        }
    }


}

/**
 *
 * @param {number|Vector3} scale
 * @param {Vector3} offset
 * @param {Vector3} direction
 * @param up
 * @param shadow
 * @param material
 * @param {Engine} engine
 */
function make_plane({
                        scale,
                        offset,
                        direction = Vector3.forward,
                        up = Vector3.up,
                        shadow = true,
                        material = new MeshBasicMaterial({ color: 0xFFFFFF }),
                        engine
                    }) {

    const t1 = Transform.fromJSON({
        position: offset,
        scale: scale
    });
    t1.rotation.lookRotation(direction, up);

    if (engine) {
        material = engine.graphics.getMaterialManager().obtain(material).getValue();
    }

    const sg = ShadedGeometry.from(new PlaneBufferGeometry(), material);

    sg.writeFlag(ShadedGeometryFlags.CastShadow, shadow);
    sg.writeFlag(ShadedGeometryFlags.ReceiveShadow, shadow);

    return new Entity()
        .add(sg)
        .add(t1);

}

function make_cornel_box(ecd) {


    make_plane({
        scale: 20,
        offset: new Vector3(10, 10, 0),
        direction: Vector3.forward,
        material: new MeshBasicMaterial({ color: 0xFF0000 })
    }).build(ecd);

    make_plane({
        scale: 20,
        offset: new Vector3(20, 10, 10),
        direction: Vector3.left,
        material: new MeshBasicMaterial({ color: 0x00FF00 })
    }).build(ecd);

    make_plane({
        scale: 20,
        offset: new Vector3(0, 10, 10),
        direction: Vector3.right,
        material: new MeshBasicMaterial({ color: 0xFFFFFF })
    }).build(ecd);

}

function make_test_texture(t = 1) {
    const t1 = (1 - t) * 0.5;
    const tex = new DataTexture(new Uint8ClampedArray([
        255 * t, 255 * t1, 255 * t1, 255,
        255 * t1, 255 * t, 255 * t1, 255,
        255 * t1, 255 * t1, 255 * t, 255,
        255 * t, 255 * t, 255 * t1, 255
    ]), 2, 2, RGBAFormat, UnsignedByteType);

    tex.flipY = false;
    tex.wrapS = ClampToEdgeWrapping;
    tex.wrapT = ClampToEdgeWrapping;
    tex.magFilter = NearestFilter;
    tex.minFilter = LinearFilter;
    tex.generateMipmaps = false;

    tex.needsUpdate = true;

    return tex;
}

/**
 *
 * @param {EntityComponentDataset} ecd
 * @param {Vector3} [center]
 * @param {Vector3} [size]
 * @param {GraphicsEngine} graphics
 * @param {number} [texture_frequency]
 */
function createFloor({
                         ecd,
                         graphics,
                         center = new Vector3(0, 0, 0),
                         size = new Vector3(1000, 1000, 1000),
                         texture_frequency = 1
                     }) {

    const map = new TextureLoader().load('data/textures/utility/checkers_dark_grey_256x256.png', t => {
        map.needsUpdate = true;

        map.wrapS = RepeatWrapping;
        map.wrapT = RepeatWrapping;

        map.repeat.set(size.x / texture_frequency, size.z / texture_frequency);

        map.matrixAutoUpdate = true;
    });


    const material = graphics.getMaterialManager().obtain(new MeshStandardMaterial({
        map: map
    })).getValue();

    const geometry = new PlaneBufferGeometry(1, 1, 1, 1);

    const q = Quaternion.fromEulerAngles(-Math.PI * 0.5, 0, 0);

    const m = new Matrix4();

    compose_matrix4_array(m.elements, Vector3.zero, q, Vector3.one);

    geometry.applyMatrix4(m);

    geometry.computeVertexNormals();
    geometry.computeTangents();

    new Entity()
        .add(Transform.fromJSON({
            position: center.toJSON(),
            // rotation: ,
            scale: size
        }))
        .add(ShadedGeometry.from(geometry, material))
        .build(ecd);

}

/**
 *
 * @param {Engine} engine
 * @return {Promise<void>}
 */
async function main(engine) {
    const volume = new LightProbeVolume();

    const transformer = new MaterialTransformer({
        volume ,
        stage: ProbeResolutionStage.Vertex
    });

    engine.graphics.getMaterialManager().addCompileStep(transformer);

    await EngineHarness.buildBasics({
        engine,
        showFps: false,
        enableWater: false,
        enableTerrain: false,
        enableLights: false,
        // enableLights: true,
        cameraFarDistance: 200,
        cameraController: false,
        focus: { x: 0, y: 0, z: 0 },
        pitch: 1,
        yaw: -1.54,
        distance: 54,
        cameraFieldOfView: focal_length_to_fov(50, 25) * RAD_TO_DEG,
        // shadowmapResolution: 4096
    });

    const sun_color = new Color();

    kelvin_to_rgb(sun_color, 0, 5000);

    await EngineHarness.buildLights({
        engine,
        shadowmapResolution: 4096,
        ambientIntensity: 0.0,
        sun: sun_color,
        // sunIntensity: 1.7,
        sunIntensity: 3,
        // sunDirection: new Vector3(0.2, -0.8, 1)
        sunDirection: new Vector3(1.2, -1, 0.2)
    })

    engine.graphics.getRenderer().setClearColor('#1e3441', 1);

    // load_and_set_cubemap_v0(engine.graphics, 'data/textures/cubemaps/hip_miramar/32/', '.png');

    const ecd = engine.entityManager.dataset;


    // const path = 'data/models/samples/sd_macross_city_standoff_diorama/scene.gltf';
    // const path = 'data/models/sibenik/2/model.gltf';
    // const path = 'data/models/vokselia_spawn/model.gltf';
    // const path = 'data/models/samples/just_a_girl/scene.gltf';
    // const path = 'data/models/samples/slum_house/scene.gltf';
    // const path = 'data/models/samples/jack_trigger/scene.gltf';
    // const path = 'data/models/samples/cornell_box/scene.gltf';
    // const path = 'data/models/Slaughter Mech/Slaugter Mech.gltf';
    // const path = 'data/models/samples/gi_box_01/model-thick-1.glb';
    // const path = 'data/models/samples/ancient_bath_house_-_modular_set/scene.gltf';
    // const path = 'data/models/samples/environment_-_library/scene.gltf';
    // const path = 'data/models/Scans/green_car_wreck/scene.gltf';
    // const path = 'data/models/samples/the_attic_environment/scene1.gltf';
    // const path = 'data/models/LowPolyTownshipSet/Small_house/Small_house.gltf';
    // const path = 'data/models/samples/cyberpunk_bike/scene.gltf';
    // const path = 'data/models/LowPolyTownshipSet/Town_Hall/model.gltf';
    // const path = 'data/models/sibenik/3-window-less/model.gltf';
    // const path = 'data/models/sponza-pbr/gltf/sponza.glb';
    // const path = 'data/models/samples/susanne.glb';
    // const path = 'data/models/samples/teapot.gltf';
    // const path = 'data/models/samples/salle_de_bain/model.glb';
    // const path = 'data/models/samples/conference/model-no-curtains.glb';
    const path = 'data/models/pica_pica/pica_pica.gltf';
    // const path = 'data/models/samples/gi_box_01/model.glb';
    // const path = 'data/models/samples/low_poly_classroom/no-glass/model.gltf';
    // const path = 'customer_data/halon_scene.glb';

    const mesh_asset = await engine.assetManager.promise(path, 'model/gltf+json');
    const gltf = mesh_asset.gltf;

    console.log(gltf);

    // add floor

    // make_cornel_box(ecd);


    const composition = three_object_to_entity_composition(gltf.scene);

    // composition.transform.rotation.multiply(Quaternion.fromEulerAngles(DEG_TO_RAD * 30, DEG_TO_RAD * 30, 0));

    composition.traverse(n => {

        /**
         * @type {ShadedGeometry}
         */
        const sg = n.entity.getComponent(ShadedGeometry);

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

        sg.writeFlag(ShadedGeometryFlags.CastShadow | ShadedGeometryFlags.ReceiveShadow, true);

        const material = sg.material;
        if (material !== null) {
            material.depthTest = true;
            material.depthWrite = true;
        }
    });

    composition.transform.scale.setScalar(1);
    //
    // composition.transform.rotation.fromEulerAngles(0, Math.PI, 0);

    composition.build(ecd);


    /**
     * @type {AABB3}
     */
    const mesh_bounds = mesh_asset.boundingBox;
    // const mesh_bounds = new AABB3(0, 0, 0, 20, 20, 20);

    createFloor({
        center: new Vector3(mesh_bounds.getCenterX(), mesh_bounds.y0, mesh_bounds.getCenterZ()),
        size: new Vector3(
            max2(0.1, mesh_bounds.getExtentsX() * 2),
            1,
            max2(0.1, mesh_bounds.getExtentsZ() * 2)
        ),
        ecd,
        graphics: engine.graphics
    });

    const model_footprint = v3_length(mesh_bounds.getExtentsX(), mesh_bounds.getExtentsY(), mesh_bounds.getExtentsZ());


    const camera = ecd.getAnyComponent(Camera);
    camera.component.clip_far = max2(100, model_footprint * 3);
    const camera_controller = ecd.getComponent(camera.entity, TopDownCameraController);
    camera_controller.distanceMax = model_footprint * 2;
    camera_controller.distanceMin = min2(camera_controller.distanceMin, model_footprint * 0.01);
    mesh_bounds.getCenter(camera_controller.target)

    const random = seededRandom();

    for (let i = 0; i < 0; i++) {

        new Entity()
            .add(Transform.fromJSON({
                position: {
                    x: randomFloatBetween(random, mesh_bounds.x0, mesh_bounds.x1),
                    y: randomFloatBetween(random, mesh_bounds.y0, mesh_bounds.y1),
                    z: randomFloatBetween(random, mesh_bounds.z0, mesh_bounds.z1),
                }
            }))
            .add(Light.fromJSON({
                distance: model_footprint * 0.1,
                type: LightType.POINT,
                intensity: 0.5,
                color: `hsv(${random()},${1},1)`
            }))
            .build(ecd);
    }

    new Entity()
        .add(GUIElement.fromView(new ButtonView({
            name: "Toggle Model",
            action() {
                composition.isBuilt ? composition.destroy() : composition.build(ecd)
            }
        })))
        .build(ecd);

    await delay(200);

    console.time('getVolume');
    // console.profile('getVolume');
    await lpv_obtain_storage_cached_volume({ engine, path, volume, density: 50 });
    // console.profileEnd('getVolume');
    console.timeEnd('getVolume');

    console.log('LPV:', volume);

    transformer.update();


    const probe_viz = lpv_visualize_probes({ volume, size: model_footprint * 0.002 });
    // probe_viz.build(ecd);

    const mesh_viz = visualize_tetrahedral_mesh({
        mesh: volume.mesh,
        positions: volume.points,
        opacity: 0.5
    });

    const depth_viz = VLPDepthMapVisualisation.from(volume).build();
    // depth_viz.build(ecd);

    // volume.visualize_mesh({ ecd, opacity: 0.5 });

    const gui = new GUI();
    gui.add(transformer, 'intensity').min(0).max(10);

    gui.add({
        capture_camera() {
            const transform = ecd.getComponent(camera.entity, Transform);

            console.log(JSON.stringify(transform.toJSON()))
        }
    }, "capture_camera");

    gui.add({
        get probe_vis() {
            return probe_viz.isBuilt
        },
        set probe_vis(v) {
            if (v) {
                probe_viz.build(ecd);
            } else {
                probe_viz.destroy();
            }
        }
    }, "probe_vis").name("Visualise Probes");
    gui.add({
        get mesh_vis() {
            return mesh_viz.isBuilt
        },
        set mesh_vis(v) {
            if (v) {
                mesh_viz.build(ecd);
            } else {
                mesh_viz.destroy();
            }
        }
    }, "mesh_vis").name("Visualise Mesh");

    gui.add({
        get depth_vis() {
            return depth_viz.isBuilt
        },
        set depth_vis(v) {
            if (v) {
                depth_viz.build(ecd);
            } else {
                depth_viz.destroy();
            }
        }
    }, "depth_vis").name("Visualise Depth");

    gui.add({
        bake(){
            volume.build_mesh();

            lpv_volume_bake_via_task(volume, ecd, engine);
        }
    },"bake").name("Bake");

    // makeDebugGizmo(engine, mesh_bounds.getCenter());


    // mesh_entity.addEventListener(SGMeshEvents.AssetLoaded, build);
    //
    // mesh_entity.build(ecd);

    lpv_build_editor({ engine, volume, path_key: path });
}

/**
 *
 * @param {Engine} engine
 * @param {Vector3} position
 */
function makeDebugGizmo(engine, position) {

    const widget = new OctahedralDepthDebuggerWidget();

    widget.entity.getComponentSafe(Transform).position.copy(position);

    widget.build(engine.entityManager.dataset, engine);
}

new EngineHarness().initialize({
    configuration(config, engine) {
        config.addSystem(new ShadedGeometrySystem(engine));
        config.addSystem(new LightSystem(engine, {
            shadowResolution: 8192
        }));
        config.addSystem(new SGMeshSystem(engine));
        config.addSystem(new TransformAttachmentSystem());
        config.addSystem(new GUIElementSystem(engine.gui.view, engine));
        config.addSystem(new ViewportPositionSystem(engine.gui.view.size));

        config.addLoader('model/gltf+json', new GLTFAssetLoader());

        config.addPlugin(GizmoRenderingPlugin);

        // config.addPlugin(AmbientOcclusionPostProcessEffect);
    }
}).then(main);
