import { is, Dataflow, Dataspace, Embedded, Reader, Ref, Schemas, Sturdy, 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,
    WebXRCamera,
} from '@babylonjs/core/Legacy/legacy';

import { activeFloorMeshes, activeTouchableMeshes, ShapeTree, scale3, builder as B } 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 }) => spawn named `sprite:${name}` {
            if (name === myId) {
                console.log('ignoring sprite', name);
            } else {
                console.log('+shape', name);
                on stop console.log('-shape', name);
                spriteMain(name, runningEngine, rootMesh, sceneDs);
            }
        }
    }
}

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

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

async function enterScene(
    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.camera.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 currentPosition = () => Shapes.Vector3(runningEngine.position);
    const currentRotation = () => Shapes.Vector3(runningEngine.rotation);

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

    const refreshPeriod = Math.floor(1000 / 10);
    at ds {
        on message timer.PeriodicTick(refreshPeriod) => {
            const newPosition = currentPosition();
            const newRotation = currentRotation();
            if (!is(Shapes.fromVector3(position.value), Shapes.fromVector3(newPosition))) {
                position.value = newPosition;
            }
            if (!is(Shapes.fromVector3(rotation.value), Shapes.fromVector3(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.Vector3,
                }) => {
                    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(id,
                                                   runningEngine,
                                                   ds,
                                                   dest.value,
                                                   newPos,
                                                   email);
                                    }
                                });
                            }
                            break;
                        default:
                            break;
                    }
                }
                const checkFacet = Turn.activeFacet;
                Turn.active.sync(sceneDs).then(() => checkFacet.turn(() => {
                    if (needStop) {
                        stop {}
                    }
                }));
            }
        }

        assert Shapes.Sprite({
            name: id,
            shape: B.nonphysical(
                B.move(position.value, B.many([
                    B.move({ x: 0, y: -0.9, z: 0 },
                           B.rotate(scale3({ x: 0, y: rotation.value.y, z: 0 }, 1 / (2 * Math.PI)),
                                    B.scale({ x: 0.4, y: 1.4, z: 0.1 }, B.box()))),
                    B.rotate(scale3(rotation.value, 1 / (2 * Math.PI)),
                             B.scale({ x: 0.15, y: 0.23, z: 0.18 }, B.many([
                                 B.box(),
                                 B.move({ 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({ x:1, y:1 }),
                                                offset: Shapes.Vector2({ x:0, y:0 }),
                                                alpha: 1
                                            }),
                                            B.rotate({ x: 0, y: 0.5, z: 0 },
                                                     B.plane()))),
                             ]))),
                ]))),
        });
    }
}

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 serverCap = Sturdy.asSturdyRef(new Reader<Ref>(
                '<ref "syndicate" [] #[pkgN9TBmEd3Q04grVG4Zdw==]>').next());
            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);
            }));

            during wsRelay.Resolved({
                "addr": relayAddr,
                "sturdyref": serverCap,
                "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(relayAddr);
                }

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

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