import { is, fromJS, Dataflow, Dataspace, Embedded, Ref, Schemas, Turn } from "@syndicate-lang/core";
import * as html from "@syndicate-lang/html";
import * as timer from "@syndicate-lang/timer";
import * as wsRelay from "@syndicate-lang/ws-relay";
import * as wakeDetector from './wake-detector.js';
import * as Shapes from './gen/shapes.js';
import * as SceneProtocol from './gen/scene.js';
import { md5 } from './md5.js';
import { setupLog, log } from './log.js';

import {
    AbstractMesh,
    Mesh,
    Vector3,
} from '@babylonjs/core/Legacy/legacy';

import { activeFloorMeshes, activeTouchableMeshes, Environment, ShapeTree, buildSound, builder as B, u2, u3, u3v } from './shapes.js';
import { RunningEngine } from './engine.js';
import { uuid } from './uuid.js';

assertion type SceneHandle(ds: Embedded<Ref>);

function interpretScene(myId: string, runningEngine: RunningEngine, rootMesh: Mesh, sceneDs: Ref) {
    at sceneDs {
        during Shapes.Sprite({ "name": $name: string, "formals": $formals }) => spawn named `sprite:${name}` {
            if (name === myId) {
                console.log('ignoring sprite', name);
            } else {
                console.log('+shape', name);
                on stop console.log('-shape', name);
                const env = new Environment(name, formals as symbol[], sceneDs);
                spriteMain(env, runningEngine, rootMesh);
            }
        }

        during SceneProtocol.AmbientSound({
            "name": $name: string,
            "spec": $spec: Shapes.SoundSpec
        }) => spawn named `sound:${name}` {
            const sound = buildSound(name, runningEngine.scene, spec, false);
            on stop sound.dispose();
        }
    }
}

function spriteMain(env: Environment, runningEngine: RunningEngine, rootMesh: Mesh) {
    at env.sceneDs {
        let currentShape = ShapeTree.empty(env.spriteName, runningEngine.scene);
        currentShape.rootnode.parent = rootMesh;
        on stop currentShape.remove();
        during Shapes.Sprite({ "name": env.spriteName, "shape": $shape: Shapes.Shape }) => {
            currentShape = currentShape.reconcile(env, env.spriteName, shape);
            currentShape.rootnode.parent = rootMesh;
        }

        during SceneProtocol.Gravity($direction: Shapes.LiteralVector3) => {
            runningEngine.applyGravity = true;
            on stop runningEngine.applyGravity = false;
            runningEngine.gravity = new Vector3(direction.x, direction.y, direction.z);
        }
    }
}

async function enterScene(
    routeField: Dataflow.Field<wsRelay.Noise.Route<Ref>>,
    id: string,
    runningEngine: RunningEngine,
    ds: Ref,
    sceneDs: Ref,
    initialPosition: Vector3,
    email: Dataflow.Field<string>,
) {
    const currentSceneFacet = Turn.activeFacet;

    console.log('enterScene', sceneDs);

    const rootMesh = new Mesh('--root-' + (+new Date()), runningEngine.scene);

    runningEngine.applyGravity = false;
    runningEngine.camera.position = initialPosition.clone();

    interpretScene(id, runningEngine, rootMesh, sceneDs);

    let lastTouchTime = 0;
    let lastTouchSpriteName = "";
    runningEngine.onCollide = (other: AbstractMesh) => {
        if (other.metadata?.touchable) {
            const now = +new Date();
            const touched = other.metadata?.spriteName ?? "";
            if ((now - lastTouchTime > 500) || (lastTouchSpriteName !== touched)) {
                currentSceneFacet.turn(() => {
                    at sceneDs {
                        send message SceneProtocol.Touch({
                            subject: id,
                            object: touched,
                        });
                    }
                });
                lastTouchTime = now;
                lastTouchSpriteName = touched;
            }
        }
    };

    const TAU = 2 * Math.PI;

    const currentPosition = () => u3(runningEngine.position);
    const currentRotation = () => u3(runningEngine.rotation, 1/TAU);

    field position: Shapes.ImmediateVector3 = currentPosition();
    field rotation: Shapes.ImmediateVector3 = currentRotation();

    const refreshPeriod = Math.floor(1000 / 10);
    at ds {
        on message timer.PeriodicTick(refreshPeriod) => {
            const newPosition = currentPosition();
            const newRotation = currentRotation();
            if (!is(Shapes.fromImmediateVector3(position.value), Shapes.fromImmediateVector3(newPosition))) {
                position.value = newPosition;
            }
            if (!is(Shapes.fromImmediateVector3(rotation.value), Shapes.fromImmediateVector3(newRotation))) {
                rotation.value = newRotation;
            }
        }
    }

    at sceneDs {
        on message SceneProtocol.Touch({ "subject": id, "object": $o }) => {
            react {
                let needStop = true;
                on asserted SceneProtocol.Portal({
                    "name": o,
                    "destination": $dest: SceneProtocol.PortalDestination,
                    "position": $targetPosition: Shapes.LiteralVector3,
                }) => {
                    const newPos = new Vector3(targetPosition.x,
                                               targetPosition.y,
                                               targetPosition.z)
                        .add(runningEngine.options.initialPos);
                    needStop = false;
                    switch (dest._variant) {
                        case "local":
                            if (dest.value === sceneDs) {
                                runningEngine.camera.position = newPos;
                            } else {
                                runningEngine.scene.removeMesh(rootMesh, true);
                                Turn.active.stop(currentSceneFacet, () => {
                                    react {
                                        enterScene(routeField,
                                                   id,
                                                   runningEngine,
                                                   ds,
                                                   dest.value,
                                                   newPos,
                                                   email);
                                    }
                                });
                            }
                            break;
                        default:
                            console.log('jumping to remote portal', dest);
                            routeField.value = dest.value;
                            break;
                    }
                }
                const checkFacet = Turn.activeFacet;
                Turn.active.sync(sceneDs).then(() => checkFacet.turn(() => {
                    if (needStop) {
                        stop {}
                    }
                }));
            }
        }

        const _POS = Symbol.for('pos');
        const _HEAD = Symbol.for('head');
        const _BODY = Symbol.for('body');

        assert Shapes.Variable({ spriteName: id, variable: _POS, value: Shapes.fromImmediateVector3(position.value) });
        assert Shapes.Variable({ spriteName: id, variable: _HEAD, value: Shapes.fromImmediateVector3(rotation.value) });
        assert Shapes.Variable({ spriteName: id, variable: _BODY, value: Shapes.fromImmediateVector3({
            x: Shapes.DoubleValue.immediate(0),
            y: rotation.value.y,
            z: Shapes.DoubleValue.immediate(0),
        }) });

        assert (() => {
            const s = Shapes.Sprite({
            name: id,
            formals: [_POS, _HEAD, _BODY],
            shape: B.nonphysical(
                B.move(Shapes.Vector3.reference(_POS), B.many([
                    B.move(u3v({x: 0, y: -0.9, z: 0}),
                           B.rotate(Shapes.Vector3.reference(_BODY),
                                    B.scale(u3v({x: 0.4, y: 1.4, z: 0.1}), B.box()))),
                    B.rotate(Shapes.Vector3.reference(_HEAD),
                             B.scale(u3v({x: 0.15, y: 0.23, z: 0.18}), B.many([
                                 B.box(),
                                 B.move(u3v({x: 0, y: 0, z: 0.501}),
                                        B.texture(
                                            Shapes.TextureSpec.uvAlpha({
                                                path: `https://www.gravatar.com/avatar/${md5(new TextEncoder().encode(email.value.trim()))}?s=256&d=wavatar`,
                                                scale: Shapes.Vector2.immediate(u2({ x:1, y:1 })),
                                                offset: Shapes.Vector2.immediate(u2({ x:0, y:0 })),
                                                alpha: Shapes.DoubleValue.immediate(1),
                                            }),
                                            B.rotate(u3v({ x: 0, y: 0.5, z: 0 }),
                                                     B.plane()))),
                             ]))),
                ]))),
            });
            console.log(s);
            return s;
        })();
    }
}

function wsurl(): string {
    const scheme = (document.location.protocol.toLowerCase() === 'https:') ? 'wss' : 'ws';
    return `${scheme}://${document.location.host}/ws`;
}

function bootApp(ds: Ref, runningEngine: RunningEngine) {
    spawn named 'app' {
        at ds {
            const id = uuid();

            const url = wsurl();
            const relayAddr = wsRelay.RelayAddress(Schemas.transportAddress.WebSocket(url));

            field email: string = localStorage.getItem('userEmail') ?? id;
            const outerFacet = Turn.activeFacet;
            const emailInput = document.getElementById('emailInput') as HTMLInputElement;
            emailInput.value = email.value;
            emailInput.addEventListener('keyup', () => outerFacet.turn(() => {
                email.value = emailInput.value;
                localStorage.setItem('userEmail', emailInput.value);
            }));

            field route: wsRelay.Noise.Route<Ref> = wsRelay.Noise.Route<Ref>({
                "transports": [fromJS(relayAddr)],
                "steps": [],
            });

            during wsRelay.Resolved({
                "route": route.value,
                "addr": $addr: wsRelay.RelayAddress,
                "resolved": $remoteDs_e: Embedded,
            }) => {
                const remoteDs = remoteDs_e.embeddedValue;

                setupLog(remoteDs, id, Symbol.for('vr-demo'));
                log('connected');

                on message wakeDetector.WakeEvent() => {
                    send message wsRelay.ForceRelayDisconnect(addr);
                }

                react {
                    at remoteDs {
                        stop on asserted SceneHandle($sceneDs_e: Embedded) => {
                            react {
                                enterScene(route,
                                           id,
                                           runningEngine,
                                           ds,
                                           sceneDs_e.embeddedValue,
                                           runningEngine.options.initialPos,
                                           email);
                            }
                        }
                    }
                }
            }
        }
    }
}

window.addEventListener('load', async () => {
    const runningEngine = await RunningEngine.start({
        floorMeshes: () => activeFloorMeshes,
        touchableMeshes: () => activeTouchableMeshes,
    });
    (window as any).E = runningEngine;
    Dataspace.boot(ds => {
        html.boot(ds);
        timer.boot(ds);
        wsRelay.boot(ds, true);
        wakeDetector.boot(ds);
        bootApp(ds, runningEngine);
    });
});
