import {Graphics} from "./Graphics";
import {Gui} from "./Gui";
import type {DebugInfos} from "./Gui";
import {xxhash128} from "hash-wasm";
import type * as RAPIER from "@dimforge/rapier2d";

type RAPIER_API = typeof import("@dimforge/rapier2d");

type Builders = Map<string, (RAPIER: RAPIER_API, testbed: Testbed) => void>;

class SimulationParameters {
    backend: string;
    prevBackend: string;
    demo: string;
    numSolverIters: number;
    running: boolean;
    stepping: boolean;
    debugRender: boolean;
    step: () => void;
    restart: () => void;
    takeSnapshot: () => void;
    restoreSnapshot: () => void;
    backends: Array<string>;
    builders: Builders;
    debugInfos: boolean;

    constructor(backends: Array<string>, builders: Builders) {
        this.backend = "rapier";
        this.prevBackend = "rapier";
        this.demo = "collision groups";
        this.numSolverIters = 4;
        this.running = true;
        this.stepping = false;
        this.debugRender = false;
        this.step = function () {};
        this.restart = function () {};
        this.takeSnapshot = function () {};
        this.restoreSnapshot = function () {};
        this.backends = backends;
        this.builders = builders;
        this.debugInfos = false;
    }
}

export class Testbed {
    RAPIER: RAPIER_API;
    gui: Gui;
    graphics: Graphics;
    inhibitLookAt: boolean;
    parameters: SimulationParameters;
    demoToken: number;
    mouse: {x: number; y: number};
    events: RAPIER.EventQueue;
    world: RAPIER.World;
    preTimestepAction?: (gfx: Graphics) => void;
    stepId: number;
    prevDemo: string;
    lastMessageTime: number;
    snap: Uint8Array;
    snapStepId: number;

    constructor(RAPIER: RAPIER_API, builders: Builders) {
        let backends = ["rapier"];
        this.RAPIER = RAPIER;
        let parameters = new SimulationParameters(backends, builders);
        this.gui = new Gui(this, parameters);
        this.graphics = new Graphics();
        this.inhibitLookAt = false;
        this.parameters = parameters;
        this.demoToken = 0;
        this.mouse = {x: 0, y: 0};
        this.events = new RAPIER.EventQueue(true);
        this.switchToDemo(builders.keys().next().value);

        window.addEventListener("mousemove", (event) => {
            this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
            this.mouse.y = 1 - (event.clientY / window.innerHeight) * 2;
        });
    }

    setpreTimestepAction(action: (gfx: Graphics) => void) {
        this.preTimestepAction = action;
    }

    setWorld(world: RAPIER.World) {
        document.onkeyup = null;
        document.onkeydown = null;
        this.preTimestepAction = null;
        this.world = world;
        this.world.numSolverIterations = this.parameters.numSolverIters;
        this.demoToken += 1;
        this.stepId = 0;
        this.gui.resetTiming();

        world.forEachCollider((coll) => {
            this.graphics.addCollider(this.RAPIER, world, coll);
        });

        this.lastMessageTime = new Date().getTime();
    }

    lookAt(pos: Parameters<Graphics["lookAt"]>[0]) {
        if (!this.inhibitLookAt) {
            this.graphics.lookAt(pos);
        }

        this.inhibitLookAt = false;
    }

    switchToDemo(demo: string) {
        if (demo == this.prevDemo) {
            this.inhibitLookAt = true;
        }

        this.prevDemo = demo;
        this.graphics.reset();
        this.stepId = 0;

        this.parameters.prevBackend = this.parameters.backend;
        this.parameters.builders.get(demo)(this.RAPIER, this);
    }

    switchToBackend(backend: string) {
        this.switchToDemo(this.parameters.demo);
    }

    takeSnapshot() {
        this.snap = this.world.takeSnapshot();
        this.snapStepId = this.stepId;
    }

    restoreSnapshot() {
        if (!!this.snap) {
            this.world.free();
            this.world = this.RAPIER.World.restoreSnapshot(this.snap);
            this.stepId = this.snapStepId;
        }
    }

    run() {
        if (this.parameters.running || this.parameters.stepping) {
            this.world.numSolverIterations = this.parameters.numSolverIters;

            if (!!this.preTimestepAction) {
                this.preTimestepAction(this.graphics);
            }

            let t0 = new Date().getTime();
            this.world.step(this.events);
            this.gui.setTiming(new Date().getTime() - t0);
            this.stepId += 1;

            if (!!this.parameters.debugInfos) {
                let t0 = performance.now();
                let snapshot = this.world.takeSnapshot();
                let t1 = performance.now();
                let snapshotTime = t1 - t0;

                let debugInfos: DebugInfos = {
                    token: this.demoToken,
                    stepId: this.stepId,
                    worldHash: "",
                    worldHashTime: 0,
                    snapshotTime: 0,
                };
                t0 = performance.now();
                xxhash128(snapshot).then((hash) => {
                    debugInfos.worldHash = hash;
                    t1 = performance.now();
                    let worldHashTime = t1 - t0;
                    debugInfos.worldHashTime = worldHashTime;
                    debugInfos.snapshotTime = snapshotTime;
                    this.gui.setDebugInfos(debugInfos);
                });
            }
        }

        if (this.parameters.stepping) {
            this.parameters.running = false;
            this.parameters.stepping = false;
        }

        this.gui.stats.begin();
        this.graphics.render(this.world, this.parameters.debugRender);
        this.gui.stats.end();

        requestAnimationFrame(() => this.run());
    }
}
