import { AxesHelper, Camera, Color, DirectionalLight, Fog, GridHelper, Mesh, MeshBasicMaterial, MeshStandardMaterial, PerspectiveCamera, PointLight, Scene, WebGLRenderer } from "three";

import { ObjectUtils, PrimitiveType } from "../engine_create_objects.js";
import { Mathf } from "../engine_math.js";
import { delay } from "../engine_utils.js";

export declare type SessionInfo = { session: XRSession, mode: XRSessionMode, init: XRSessionInit };

/** Create with static `start`- used to start an XR session while waiting for session granted */
export class TemporaryXRContext {

    private static _active: TemporaryXRContext | null = null;
    static get active() {
        return this._active;
    }

    private static _requestInFlight = false;

    static async start(mode: XRSessionMode, init: XRSessionInit) {
        if (this._active) {
            console.error("Cannot start a new XR session while one is already active");
            return null;
        }
        if (this._requestInFlight) {
            console.error("Cannot start a new XR session while a request is already in flight");
            return null;
        }

        if ('xr' in navigator && navigator.xr) {
            if (!init) {
                console.error("XRSessionInit must be provided");
                return null;
            }
            this._requestInFlight = true;
            const session = await navigator.xr.requestSession(mode, init);
            session.addEventListener("end", () => {
                this._active = null;
            });
            if (!this._requestInFlight) {
                session.end();
                return null;
            }
            this._requestInFlight = false;
            this._active = new TemporaryXRContext(mode, init, session);
            return this._active;
        }

        return null;
    }

    static async handoff(): Promise<SessionInfo | null> {
        if (this._active) {
            return this._active.handoff();
        }
        return null;
    }

    static async stop() {
        this._requestInFlight = false;
        if (this._active) {
            await this._active.end();
            await delay(100);
        }
        this._active = null;
    }

    private readonly _session: XRSession | null;
    private readonly _mode: XRSessionMode;
    private readonly _init: XRSessionInit;

    get isAR() {
        return this._mode === "immersive-ar";
    }

    private readonly _renderer: WebGLRenderer;
    private readonly _camera: Camera;
    private readonly _scene: Scene;

    private constructor(mode: XRSessionMode, init: XRSessionInit, session: XRSession) {
        this._mode = mode;
        this._init = init;
        this._session = session;
        this._session.addEventListener("end", this.onEnd);

        this._renderer = new WebGLRenderer({ alpha: true });
        this._renderer.setAnimationLoop(this.onFrame);
        this._renderer.xr.setSession(session);
        this._renderer.xr.enabled = true;
        this._camera = new PerspectiveCamera();
        this._scene = new Scene();
        this._scene.fog = new Fog(0x444444, 10, 250);
        this._scene.add(this._camera);
        this.setupScene();
    }

    end() {
        if (!this._session) return Promise.resolve();
        return this._session.end();
    }

    /** returns the session and session info and stops the temporary rendering */
    async handoff() {
        if (!this._session) throw new Error("Cannot handoff a session that has already ended");
        const info: SessionInfo = {
            session: this._session,
            mode: this._mode,
            init: this._init
        };
        await this.onBeforeHandoff();
        // calling onEnd here directly because we dont end the session
        this.onEnd();
        // set the session to null because we dont want this class to accidentaly end the session
        //@ts-ignore
        this._session = null;
        return info;
    }

    private onEnd = () => {
        this._session?.removeEventListener("end", this.onEnd);
        this._renderer.setAnimationLoop(null);
        this._renderer.dispose();
        this._scene.clear();
    }

    private _lastTime = 0;
    private onFrame = (time: DOMHighResTimeStamp, _frame: XRFrame) => {
        const dt = time - this._lastTime;
        this.update(time, dt);
        if (this._camera.parent !== this._scene) {
            this._scene.add(this._camera);
        }
        this._renderer.render(this._scene, this._camera);
    }

    /** can be used to prepare the user or fade to black */
    private async onBeforeHandoff() {
        // for(const sphere of this._spheres) {
        //     sphere.removeFromParent();
        //     await delay(10);
        // }

        // const obj = ObjectUtils.createPrimitive(PrimitiveType.Cube);
        // obj.position.z = -3;
        // obj.position.y = .5;
        // this._scene.add(obj); 
        await delay(1000);
        this._scene.clear();
        // await delay(100);
    }


    private _objects: Mesh[] = [];
    private setupScene() {
        this._scene.background = new Color(0x000000);
        this._scene.add(new GridHelper(5, 10, 0x111111, 0x111111));

        const light = new DirectionalLight(0xffffff, 1);
        light.position.set(0, 20, 0);
        light.castShadow = false;
        this._scene.add(light);

        const light2 = new DirectionalLight(0xffffff, 1);
        light2.position.set(0, -1, 0);
        light2.castShadow = false;
        this._scene.add(light2);

        const light3 = new PointLight(0xffffff, 1, 100, 1);
        light3.position.set(0, 2, 0);
        light3.castShadow = false;
        light3.distance = 200;
        this._scene.add(light3);

        const range =  50;
        for (let i = 0; i < 100; i++) {
            const material = new MeshStandardMaterial({
                color: 0x222222,
                metalness: 1,
                roughness: .8,
            });
            // if we're in passthrough
            if (this.isAR) {
                material.emissive = new Color(Math.random(), Math.random(), Math.random());
                material.emissiveIntensity = Math.random();
            }
            const type = Mathf.random(0, 1) > .5 ? PrimitiveType.Sphere : PrimitiveType.Cube;
            const obj = ObjectUtils.createPrimitive(type, {
                material
            });
            obj.position.x = Mathf.random(-range, range);
            obj.position.y = Mathf.random(-2, range);
            obj.position.z = Mathf.random(-range, range);
            // random rotation
            obj.rotation.x = Mathf.random(0, Math.PI * 2);
            obj.rotation.y = Mathf.random(0, Math.PI * 2);
            obj.rotation.z = Mathf.random(0, Math.PI * 2);
            obj.scale.multiplyScalar(.5 + Math.random() * 10);

            const dist = obj.position.distanceTo(this._camera.position) - obj.scale.x;
            if (dist < 1) {
                obj.position.multiplyScalar(1 + 1 / dist);
            }

            this._objects.push(obj);
            this._scene.add(obj);
        }
    }

    private update(time: number, _deltaTime: number) {

        const speed = time * .0004;
        for (let i = 0; i < this._objects.length; i++) {
            const obj = this._objects[i];
            obj.position.y += Math.sin(speed + i * .5) * 0.005;
            obj.rotateY(.002);
        }
    }
}