import { GUI } from "dat.gui";
import { MeshBasicMaterial, MeshStandardMaterial, OctahedronBufferGeometry } from "three";
import { BinaryDataType } from "../../../core/binary/type/BinaryDataType.js";
import { Color } from "../../../core/color/Color.js";
import { max2 } from "../../../core/math/max2.js";
import { min2 } from "../../../core/math/min2.js";
import { randomFloatBetween } from "../../../core/math/random/randomFloatBetween.js";
import { seededRandom } from "../../../core/math/random/seededRandom.js";
import Clock from "../../Clock.js";
import Entity from "../../ecs/Entity.js";
import { Transform } from "../../ecs/transform/Transform.js";
import { EngineHarness } from "../../EngineHarness.js";
import { ShadedGeometry } from "../../graphics/ecs/mesh-v2/ShadedGeometry.js";
import { ShadedGeometrySystem } from "../../graphics/ecs/mesh-v2/ShadedGeometrySystem.js";
import Trail2D from "../../graphics/ecs/trail2d/Trail2D.js";
import Trail2DSystem from "../../graphics/ecs/trail2d/Trail2DSystem.js";
import {
    RGBA_LUT_HEATMAP_IR
} from "../../graphics/particles/particular/engine/parameter/sample/RGBA_LUT_HEATMAP_IR.js";
import { SequenceBehavior } from "../../intelligence/behavior/composite/SequenceBehavior.js";
import { RepeatBehavior } from "../../intelligence/behavior/decorator/RepeatBehavior.js";
import { BehaviorComponent } from "../../intelligence/behavior/ecs/BehaviorComponent.js";
import { BehaviorSystem } from "../../intelligence/behavior/ecs/BehaviorSystem.js";
import { ActionBehavior } from "../../intelligence/behavior/primitive/ActionBehavior.js";
import { RandomDelayBehavior } from "../../intelligence/behavior/util/RandomDelayBehavior.js";
import { TimeSeries } from "./TimeSeries.js";

const harness = new EngineHarness();

/**
 *
 * @param {EntityComponentDataset} ecd
 * @param {TimeSeries} table
 * @param {number} [count]
 */
function makeGhostMarkers({
                              ecd,
                              table,
                              count = 5
                          }) {


    const g = new OctahedronBufferGeometry(1, 3);

    /**
     *
     * @type {Entity[]}
     */
    const entities = [];

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

        const color = new Color();

        const sample = [];

        RGBA_LUT_HEATMAP_IR.sample(i / (count - 1), sample);

        color.setRGBUint8(...sample);

        const transform = new Transform();

        const entity = new Entity();

        entities.push(entity);

        entity
            .add(transform)
            .add(ShadedGeometry.from(g, new MeshBasicMaterial({
                wireframe: true,
                color: color.toUint()
            })))
            .build(ecd);
    }

    return {
        set time(time) {

            const sample = table.sampleObjectLinear(time);

            const ref_index = table.findLowSampleIndexByTime(time);

            const start_index = max2(0, ref_index - Math.floor(count / 2));
            const limit_index = min2(table.sample_count - 1, ref_index + Math.ceil(count / 2));

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

                const entity = entities[i];

                const sample_index = ref_index - Math.floor(count / 2) + i;

                const out_of_bound_marker = sample_index < start_index || sample_index > limit_index;

                if (out_of_bound_marker) {
                    if (entity.isBuilt) {
                        entity.destroy();
                    }
                } else {
                    if (!entity.isBuilt) {
                        entity.build(ecd);
                    }
                }

                if (out_of_bound_marker) {
                    continue;
                }

                const transform = entity.getComponentSafe(Transform);

                const sample = table.getSampleObjectByIndex(sample_index);

                transform.position.set(
                    sample.x,
                    sample.y,
                    sample.z
                );

            }
        }
    };
}

/**
 *
 * @param {Engine} engine
 * @return {Promise<void>}
 */
async function main(engine) {
    await EngineHarness.buildBasics({
        engine
    });

    const timeSeries = new TimeSeries({
        time: BinaryDataType.Float64,
        x: BinaryDataType.Float32,
        y: BinaryDataType.Float32,
        z: BinaryDataType.Float32,
    }, 'time');

    const random = seededRandom(42);

    timeSeries.addSample([
        0, 0, 1, 0
    ]);

    const sim_clock = new Clock();
    sim_clock.start();

    function addSample() {

        const time_delta = sim_clock.getDelta();

        // get last row
        const last_record = timeSeries.getSampleObjectByIndex(timeSeries.sample_count - 1);

        const speed = 7;

        const displacement = speed * time_delta;

        const direction = randomFloatBetween(random, -Math.PI, Math.PI);

        const displacement_x = displacement * Math.cos(direction);
        const displacement_y = displacement * Math.sin(direction);

        const new_x = last_record.x + displacement_x;
        const new_z = last_record.z + displacement_y;

        timeSeries.addSample([
            last_record.time + time_delta,
            new_x,
            1,
            new_z,
        ]);
    }


    const ecd = engine.entityManager.dataset;

    // simulate data arriving from the network
    const sample_delay = RandomDelayBehavior.from(0.5, 1);
    new Entity()
        .add(BehaviorComponent.from(RepeatBehavior.from(
            SequenceBehavior.from([
                sample_delay,
                new ActionBehavior(addSample)
            ])
        )))
        .build(ecd);

    const sim_parameters = {
        playing: true,
        play_time: -1,
        get latest_data_time() {
            return timeSeries.last_timestamp;
        },
        get behind_latest() {
            return timeSeries.last_timestamp - this.play_time;
        },
        set behind_latest(v) {

            const current_delay = timeSeries.last_timestamp - this.play_time;

            const delta = v - current_delay;

            this.play_time += delta;
        }
    };


    // simulate agent moving through space
    const subject = new Entity();

    subject
        .add(Transform.fromJSON({
            position: 0
        }))
        .add(Trail2D.fromJSON({
            textureURL: "data/textures/trail/Circle_04.png",
            width: 0.2
        }))
        .add(ShadedGeometry.from(new OctahedronBufferGeometry(1, 5), new MeshStandardMaterial()))
        .add(BehaviorComponent.from(RepeatBehavior.from(
            new ActionBehavior((delta) => {
                if (sim_parameters.playing) {
                    sim_parameters.play_time += delta;
                }

                const sample = timeSeries.sampleObjectLinear(sim_parameters.play_time);

                const transform = subject.getComponentSafe(Transform);

                transform.position.set(
                    sample.x,
                    sample.y,
                    sample.z,
                );
            })
        )))
        .build(ecd);

    const markers = makeGhostMarkers({
        ecd, table: timeSeries, count: 5
    });

    engine.ticker.onTick.add(() => markers.time = sim_parameters.play_time);

    const gui = new GUI();

    const gui_samples = gui.addFolder("samples");
    gui_samples.add(sample_delay.limits, 'min').name("min_delay").step(0.01)
    gui_samples.add(sample_delay.limits, 'max').name("max_delay").step(0.01)

    for (const k in sim_parameters) {

        const control = gui.add(sim_parameters, k);

        if (typeof sim_parameters[k] === "number") {
            control.step(0.01);
        }

        let last_value = sim_parameters[k];

        engine.ticker.onTick.add(() => {
            let should_update = true;

            const current_value = sim_parameters[k];
            if (last_value === current_value) {
                should_update = false;
            } else {
                last_value = current_value;
            }


            if (should_update) {
                control.updateDisplay();
            }
        })
    }

    gui.open();

}

harness.initialize({
    configuration(config, engine) {
        config.addManySystems(
            new BehaviorSystem(engine),
            new ShadedGeometrySystem(engine),
            new Trail2DSystem(engine)
        )
    }
}).then(main);
