import { OctahedronBufferGeometry } from "three";
import { TransformControls } from "../../../../editor/tools/v2/TransformControls.js";
import { TransformMode } from "../../../../editor/tools/v2/TransformMode.js";
import { assert } from "../../../core/assert.js";
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
import { downloadAsFile } from "../../../core/binary/downloadAsFile.js";
import { EndianType } from "../../../core/binary/EndianType.js";
import { SignalBinding } from "../../../core/events/signal/SignalBinding.js";
import { SimpleStateMachine } from "../../../core/fsm/simple/SimpleStateMachine.js";
import { SimpleStateMachineDescription } from "../../../core/fsm/simple/SimpleStateMachineDescription.js";
import { SurfacePoint3 } from "../../../core/geom/3d/SurfacePoint3.js";
import Vector3 from "../../../core/geom/Vector3.js";
import { Action } from "../../../core/process/undo/Action.js";
import { ActionProcessor } from "../../../core/process/undo/ActionProcessor.js";
import { ArrayBufferLoader } from "../../asset/loaders/ArrayBufferLoader.js";
import Tag from "../../ecs/components/Tag.js";
import Entity from "../../ecs/Entity.js";
import { Transform } from "../../ecs/transform/Transform.js";
import { EngineHarness } from "../../EngineHarness.js";
import Highlight from "../ecs/highlight/Highlight.js";
import { ShadedGeometryHighlightSystem } from "../ecs/highlight/system/ShadedGeometryHighlightSystem.js";
import { ShadedGeometry } from "../ecs/mesh-v2/ShadedGeometry.js";
import { ShadedGeometrySystem } from "../ecs/mesh-v2/ShadedGeometrySystem.js";
import { make_ray_from_viewport_position } from "../make_ray_from_viewport_position.js";
import { MaterialTransformer } from "./gi/material/MaterialTransformer.js";
import { lpv_volume_bake_via_task } from "./lpv/build_probes_for_scene.js";
import { LightProbeVolumeSerializationAdapter } from "./lpv/serialization/LightProbeVolumeSerializationAdapter.js";
import { sh3_make_shaded_geometry } from "./visualise_spherical_harmonic_sphere.js";

const DEFAULT_SH_DATA = new Float32Array(27);
DEFAULT_SH_DATA.fill(1);

class ActionProbeRemove extends Action {

    entity = -1;

    _index = -1;

    /**
     * @type {Entity}
     * @private
     */
    _destroyed;

    static from(entity) {
        const r = new ActionProbeRemove();

        r.entity = entity;

        return r;
    }

    /**
     *
     * @param {EditorState} context
     * @return {Promise<void>}
     */
    async apply(context) {

        const probe_index = context.getProbeIndex(this.entity);

        this._index = probe_index;

        this._destroyed = context.removeProbe(probe_index);
    }

    /**
     *
     * @param {EditorState} context
     * @return {Promise<void>}
     */
    async revert(context) {

        this.entity = this._destroyed.id;

        context.createProbe(this._destroyed);

        this._destroyed = null;
        this._index = -1;

    }

}

class EditorState {
    /**
     *
     * @type {Entity[]}
     */
    probes = [];
    /**
     *
     * @type {LightProbeVolume|null}
     */
    volume = null

    /**
     * @type {Engine}
     */
    engine

    mesh_needs_rebuilding = false;

    hideProbes(){
        this.probes.forEach(p => p.destroy());
    }

    showProbes(){
        const ecd = this.ecd;
        this.probes.forEach(p => p.build(ecd));
    }

    ensureMesh() {
        if (this.mesh_needs_rebuilding) {
            this.volume.build_mesh();
            this.mesh_needs_rebuilding = false;
        }
    }

    /**
     *
     * @return {EntityComponentDataset}
     */
    get ecd() {
        return this.engine.entityManager.dataset;
    }

    /**
     *
     * @param {Entity} entity
     */
    createProbe(entity) {

        entity.build(this.ecd);

        this.probes.push(entity);

        const p = entity.getComponent(Transform).position;

        const index = this.volume.add_point(...p);

        assert.equal(index, this.probes.length - 1);

        this.mesh_needs_rebuilding = true;
    }

    /**
     *
     * @param {number} index
     * @return {Entity}
     */
    removeProbe(index) {
        const [probe] = this.probes.splice(index, 1);

        probe.destroy();

        this.volume.remove_point(index);

        this.mesh_needs_rebuilding = true;

        return probe;
    }

    probeCoefficientsFromVolume(index) {
        const probe = this.probes[index];

        const sg = probe.getComponent(ShadedGeometry);

        sg.material.fromArray(this.volume.harmonics, index * 27);
    }

    /**
     *
     * @param {number} index
     */
    probeToVolume(index) {

        const probe = this.probes[index];
        const t = probe.getComponent(Transform);

        const existing_position = new Vector3();
        existing_position.fromArray(this.volume.points, index * 3);

        if (existing_position.equals(t.position)) {
            // all good
            return;
        }

        t.position.toArray(this.volume.points, index * 3);
        this.volume.incrementVersion();

        this.mesh_needs_rebuilding = true;
    }

    /**
     *
     * @param {number} entity_id
     * @return {number}
     */
    getProbeIndex(entity_id) {
        return this.probes.findIndex(p => p.id === entity_id);
    }
}

/**
 *
 * @param {Engine} engine
 * @param {LightProbeVolume} volume
 * @param {string} [path_key]
 * @param {number} [placement_surface_offset] How far to place new probes away from the surface
 * @param {number} [probe_size] purely visual, how big is each probe when visualised?
 */
export async function lpv_build_editor({
                                           engine,
                                           volume,
                                           path_key = "lpv",
                                           placement_surface_offset = 1,
                                            probe_size = 1,
                                       }) {

    engine.assetManager.registerLoader('binary', new ArrayBufferLoader());


    let build_in_progress = false;

    async function bake() {
        if (build_in_progress) {
            console.warn('build in progress');
            return;
        }
        build_in_progress = true;

        ctx.hideProbes();

        volume.build_mesh();

        await lpv_volume_bake_via_task(volume, ecd, engine);

        for (let i = 0; i < ctx.probes.length; i++) {
            ctx.probeCoefficientsFromVolume(i);
        }

        ctx.showProbes();

        update_volume_visual();

        build_in_progress = false;

    }

    // create drop handle
    document.body.addEventListener('dragover', (ev) => {
        ev.preventDefault();
    });
    document.body.addEventListener("drop",
        /**
         *
         * @param {DragEvent} ev
         */
        (ev) => {
            ev.preventDefault();

            let processed = false;

            /**
             *
             * @param {File} file
             */
            function processFile(file) {
                if (processed) {
                    return;
                }

                // URL @ Mozilla, webkitURL @ Chrome
                const url = (window.webkitURL ? webkitURL : URL).createObjectURL(file);

                engine.assetManager.promise(url, "binary").then(asset => {

                    const data = asset.create();

                    const buffer = BinaryBuffer.fromArrayBuffer(data);
                    buffer.endianness = EndianType.LittleEndian;

                    const adapter = new LightProbeVolumeSerializationAdapter();

                    adapter.deserialize(buffer, volume);

                    // assume mesh is invalid just in case
                    ctx.mesh_needs_rebuilding = true;

                    // reinit
                    reinit();
                    update_volume_visual();

                });


                processed = true;
            }

            if (ev.dataTransfer.items) {
                // Use DataTransferItemList interface to access the file(s)
                for (var i = 0; i < ev.dataTransfer.items.length; i++) {
                    // If dropped items aren't files, reject them
                    if (ev.dataTransfer.items[i].kind === 'file') {
                        var file = ev.dataTransfer.items[i].getAsFile();

                        processFile(file);
                    }
                }
            } else {
                // Use DataTransfer interface to access the file(s)
                for (var i = 0; i < ev.dataTransfer.files.length; i++) {
                    const file = ev.dataTransfer.files[i];

                    processFile(file);
                }
            }
        })

    const camera_controller = await EngineHarness.buildOrbitalCameraController({
        engine
    });
    camera_controller.destroy();


    const em = engine.entityManager;
    const ecd = em.dataset;

    em.addSystem(new ShadedGeometryHighlightSystem(engine));

    ecd.registerComponentType(Highlight);

    const PROBE_TAG_STRING = 'PLV/probe';

    const geometry = new OctahedronBufferGeometry(1, 5);

    const ctx = new EditorState();
    ctx.probes = [];
    ctx.volume = volume;
    ctx.engine = engine;

    /**
     *
     * @type {number[]}
     */
    const selected = [];

    const actions = new ActionProcessor(ctx);

    const controls = new TransformControls(engine.graphics.camera, engine.viewStack.el);
    controls.setMode(TransformMode.Translate);
    controls.build(ecd);
    controls.detach();


    function unselect() {
        if (selected.length === 0) {
            return;
        }

        for (let i = 0; i < selected.length; i++) {

            ecd.removeComponentFromEntity(selected[i], Highlight);

        }

        selected.splice(0, selected.length);

    }

    /**
     *
     * @param {number[]} entities
     */
    function set_selection(entities) {
        unselect();

        for (let i = 0; i < entities.length; i++) {
            const entity = entities[i];

            ecd.addComponentToEntity(entity, Highlight.fromOne(1, 1, 0));
            selected.push(entity);

        }

    }

    function create_probe_entity(volume_index = -1) {

        const entity = new Entity();

        const transform = new Transform();

        transform.scale.setScalar(probe_size);

        entity.add(Tag.fromOne(PROBE_TAG_STRING));
        entity.add(transform);

        let sg;
        if (volume_index >= 0) {

            sg = sh3_make_shaded_geometry(
                ctx.volume.harmonics, 27 * volume_index
            );
        } else {
            sg = sh3_make_shaded_geometry(DEFAULT_SH_DATA, 0);
        }

        entity.add(sg);

        return entity;
    }

    const smd = new SimpleStateMachineDescription();
    const STATE_NAVIGATION = smd.createState();
    const STATE_TRANSFORM = smd.createState();

    smd.createEdge(STATE_NAVIGATION, STATE_TRANSFORM);
    smd.createEdge(STATE_TRANSFORM, STATE_NAVIGATION);

    const transform_proxy = new Entity().add(new Transform());
    transform_proxy.build(ecd);

    /**
     *
     * @param {number[]} entities
     */
    function attach_transform(entities) {
        const entities_copy = entities.slice();

        // figure out center mass
        const center = new Vector3();
        for (let i = 0; i < entities.length; i++) {
            const entity = entities[i];

            center.add(
                ecd.getComponent(entity, Transform).position
            );
        }
        center.multiplyScalar(1 / entities.length);
        transform_proxy.getComponent(Transform).position.copy(center);

        const last_position = new Vector3();
        last_position.copy(center);

        controls.attach(transform_proxy.id);
        controls.entity.addEventListener("change", () => {
            if (entities_copy.length === 0) {
                return;
            }

            const new_position = transform_proxy.getComponent(Transform).position.clone();
            const displacement = new_position.clone();
            displacement.sub(last_position);

            last_position.copy(new_position);

            for (let i = 0; i < entities.length; i++) {
                const entity = entities[i];
                const index = ctx.getProbeIndex(entity);

                const probe = ctx.probes[index];

                probe.getComponent(Transform).position.add(displacement);

                ctx.probeToVolume(index);
            }
        })
    }

    const sm = new SimpleStateMachine(smd);

    const keys = engine.devices.keyboard.keys;

    function register_shortcuts_common() {

        const ctrl = keys.ctrl;

        keys.l.down.add(reinit);
        keys.b.down.add(bake);

        keys.z.down.add(() => {

            if (ctrl.is_down) {
                actions.undo();
            }

        });

        keys.y.down.add(() => {

            if (ctrl.is_down) {
                actions.redo();
            }

        });

        keys.s.down.add(() => {

            if (ctrl.is_down) {

                ctx.ensureMesh();

                const buffer = BinaryBuffer.fromEndianness(EndianType.LittleEndian);

                const adapter = new LightProbeVolumeSerializationAdapter();

                adapter.serialize(buffer, volume);

                buffer.trim();

                engine.storage.promiseStoreBinary(`lpv:${path_key}`, buffer.data);


                const date = new Date();

                downloadAsFile(buffer.data, `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}.lpv`)
            }

        });
    }


    const state_bindings_nav = [
        new SignalBinding(engine.devices.pointer.on.tap, () => {
            const ray = make_ray_from_viewport_position(engine, engine.devices.pointer.position);

            const sgm = em.getSystem(ShadedGeometrySystem);

            const contact = new SurfacePoint3();

            if (keys.ctrl.is_down) {
                // create new entity
                const entity = sgm.raycastNearest(
                    contact,
                    ...ray.origin,
                    ...ray.direction,
                    (entity) => {
                        return !(ecd.getComponent(entity, Tag)?.contains(PROBE_TAG_STRING));
                    }
                );

                if (entity !== undefined) {


                    const probe = create_probe_entity();

                    const p = new Vector3();
                    p.copy(contact.normal);
                    p.multiplyScalar(0.01);
                    p.add(contact.position);

                    const p2 = new SurfacePoint3();

                    // do raycast in opposite direction
                    const entity_2 = sgm.raycastNearest(
                        p2,
                        ...p,
                        ...contact.normal,
                        (entity) => {
                            return !(ecd.getComponent(entity, Tag)?.contains(PROBE_TAG_STRING));
                        }
                    );

                    if (entity_2 !== undefined && p2.position.distanceTo(p) < placement_surface_offset * 2) {

                        p.lerpVectors(p, p2.position, 0.5);

                    } else {
                        p.copy(contact.normal);
                        p.multiplyScalar(placement_surface_offset);
                        p.add(contact.position);
                    }

                    probe.getComponent(Transform).position.copy(p);

                    ctx.createProbe(probe);

                    set_selection([probe.id]);

                }

            } else {
                // do select
                const entity = sgm.raycastNearest(
                    contact,
                    ...ray.origin,
                    ...ray.direction,
                    (entity) => {
                        return ecd.getComponent(entity, Tag)?.contains(PROBE_TAG_STRING);
                    }
                );

                if (entity !== undefined) {

                    const entity_id = entity.entity;

                    if (keys.shift.is_down) {
                        if (selected.includes(entity_id)) {
                            set_selection(selected.filter(e => e !== entity_id));
                        } else {
                            set_selection(selected.slice().concat(entity_id));
                        }
                    } else {
                        set_selection([entity_id]);
                    }
                }
            }
        }),
        new SignalBinding(keys.x.down, () => {

            if (selected.length === 0) {
                return;
            }

            const entities = selected.slice();

            unselect();

            actions.mark('Delete');
            for (let i = 0; i < entities.length; i++) {
                actions.do(ActionProbeRemove.from(entities[i]));
            }
        }),
        new SignalBinding(keys.e.down, () => {
            if (selected.length === 0) {
                return;
            }

            sm.navigateTo(STATE_TRANSFORM);
        }),
        new SignalBinding(keys.a.down, () => {
            if (keys.ctrl.is_down) {
                // select all
                set_selection(ctx.probes.map(e => e.id));
            }
        }),
        new SignalBinding(keys.h.down,()=>{

            if(ctx.probes.length === 0){
                return;
            }

            if(ctx.probes[0].isBuilt){
                ctx.hideProbes();
            }else{
                ctx.showProbes();
            }

        })
    ];

    sm.addEventHandlerStateEntry(STATE_NAVIGATION, () => {
        camera_controller.build(ecd);

        state_bindings_nav.forEach(b => b.link());
    });
    sm.addEventHandlerStateExit(STATE_NAVIGATION, () => {
        camera_controller.destroy();

        state_bindings_nav.forEach(b => b.unlink())
    });

    const state_bindings_transform = [

        new SignalBinding(keys.e.down, () => {
            sm.navigateTo(STATE_NAVIGATION);
        }),
    ];

    sm.addEventHandlerStateEntry(STATE_TRANSFORM, () => {
        state_bindings_transform.forEach(b => b.link());

        attach_transform(selected);
    });
    sm.addEventHandlerStateExit(STATE_TRANSFORM, () => {
        state_bindings_transform.forEach(b => b.unlink());

        controls.detach();
    });

    sm.setState(STATE_NAVIGATION);

    function initialize_from_volume() {

        for (let i = 0; i < volume.count; i++) {

            const entity = create_probe_entity(i);

            entity.getComponent(Transform).position.fromArray(volume.points, i * 3);

            ctx.probes.push(entity);

            entity.build(ecd);
        }

    }

    function update_volume_visual() {
        const transformers = engine.graphics.getMaterialManager().getCompilationSteps(MaterialTransformer);

        ctx.ensureMesh();

        transformers.forEach(t => t.update());
    }

    function reinit() {
        unselect();
        ctx.probes.forEach(p => p.destroy());
        ctx.probes.splice(0, ctx.probes.length);
        initialize_from_volume();

        console.log(volume);
    }


    initialize_from_volume();
    register_shortcuts_common();
}