import {
    Camera,
    MathUtils,
    NoToneMapping,
    PCFSoftShadowMap,
    Scene,
    ShadowMapType,
    ToneMapping,
    WebGLRenderer,
} from 'three';

export type DIVERendererSettings = {
    antialias: boolean;
    alpha: boolean;
    stencil: boolean;
    shadowMapEnabled: boolean;
    shadowMapType: ShadowMapType;
    toneMapping: ToneMapping;
    canvas?: HTMLCanvasElement;
};

export const DIVERendererDefaultSettings: DIVERendererSettings = {
    antialias: true,
    alpha: true,
    stencil: false,
    shadowMapEnabled: true,
    shadowMapType: PCFSoftShadowMap,
    toneMapping: NoToneMapping,
    canvas: undefined,
};

export type DIVERenderCallback = (
    time: DOMHighResTimeStamp,
    frame: XRFrame,
) => void;

/**
 * A changed version of the WebGLRenderer.
 *
 * Has to be started manually by calling StartRenderer().
 *
 * @module
 */

export class DIVERenderer extends WebGLRenderer {
    // basic functionality members
    private paused: boolean = false;
    private running: boolean = false;
    private force: boolean = false;

    // pre- and post-render callbacks
    private preRenderCallbacks: Map<string, DIVERenderCallback> = new Map<
        string,
        DIVERenderCallback
    >();
    private postRenderCallbacks: Map<string, DIVERenderCallback> = new Map<
        string,
        DIVERenderCallback
    >();

    constructor(
        rendererSettings: Partial<DIVERendererSettings> = DIVERendererDefaultSettings,
    ) {
        super({
            antialias:
                rendererSettings.antialias ||
                DIVERendererDefaultSettings.antialias,
            alpha: rendererSettings.alpha || DIVERendererDefaultSettings.alpha,
            preserveDrawingBuffer: true,
            canvas: rendererSettings.canvas,
        });
        this.setPixelRatio(window.devicePixelRatio);

        this.shadowMap.enabled =
            rendererSettings.shadowMapEnabled ||
            DIVERendererDefaultSettings.shadowMapEnabled;
        this.shadowMap.type =
            rendererSettings.shadowMapType ||
            DIVERendererDefaultSettings.shadowMapType;

        this.toneMapping =
            rendererSettings.toneMapping ||
            DIVERendererDefaultSettings.toneMapping;

        this.debug.checkShaderErrors = false;
    }

    // Stops renderings and disposes the renderer.
    public Dispose(): void {
        this.StopRenderer();
        this.dispose();
    }

    // Starts the renderer with the given scene and camera.
    public StartRenderer(scene: Scene, cam: Camera): void {
        this.setAnimationLoop((time: DOMHighResTimeStamp, frame: XRFrame) => {
            this.internal_render(scene, cam, time, frame);
        });
        this.running = true;
    }

    // Pauses the renderer.
    public PauseRenderer(): void {
        this.paused = true;
    }

    // Resumes the renderer after pausing.
    public ResumeRenderer(): void {
        this.paused = false;
    }

    // Stops the renderer completely. Has to be started again with StartRenderer().
    public StopRenderer(): void {
        this.setAnimationLoop(null);
        this.running = false;
    }

    // Resizes the renderer to the given width and height.
    public OnResize(width: number, height: number): void {
        this.setSize(width, height);
    }

    /**
     * Adds a callback to the render loop before actual render call.
     * @param callback Executed before rendering.
     * @returns uuid to remove the callback.
     */
    public AddPreRenderCallback(callback: DIVERenderCallback): string {
        // add callback to renderloop
        const newUUID = MathUtils.generateUUID();
        this.preRenderCallbacks.set(newUUID, callback);

        return newUUID;
    }

    /**
     * Removes a callback from the render loop before actual render call.
     * @param uuid of callback to remove.
     * @returns if removing was successful.
     */
    public RemovePreRenderCallback(uuid: string): boolean {
        // check if callback exists
        if (!this.preRenderCallbacks.has(uuid)) return false;

        // remove callback from renderloop
        this.preRenderCallbacks.delete(uuid);

        return true;
    }

    /**
     * Adds a callback to the render loop after actual render call.
     * @param callback Executed after rendering.
     * @returns uuid to remove the callback.
     */
    public AddPostRenderCallback(callback: DIVERenderCallback): string {
        // add callback to renderloop
        const newUUID = MathUtils.generateUUID();
        this.postRenderCallbacks.set(newUUID, callback);

        return newUUID;
    }

    /**
     * Removes a callback from the render loop after actual render call.
     * @param uuid of callback to remove.
     * @returns if removing was successful.
     */
    public RemovePostRenderCallback(uuid: string): boolean {
        // check if callback exists
        if (!this.postRenderCallbacks.has(uuid)) return false;

        // remove callback from renderloop
        this.postRenderCallbacks.delete(uuid);

        return true;
    }

    /**
     * Forces the renderer to render the next frame.
     */
    public ForceRendering(): void {
        this.force = true;
    }

    /**
     * Internal render loop.
     *
     * To control renderloop you can add callbacks via AddPreRenderCallback() and AddPostRenderCallback().
     * @param scene Scene to render.
     * @param cam Camera to render with.
     */
    private internal_render(
        scene: Scene,
        cam: Camera,
        time: DOMHighResTimeStamp,
        frame: XRFrame,
    ): void {
        // execute background render loop callbacks
        if ((this.paused || !this.running) && !this.force) return;

        // execute render loop callbacks
        this.preRenderCallbacks.forEach((callback) => {
            callback(time, frame);
        });

        this.render(scene, cam);

        this.postRenderCallbacks.forEach((callback) => {
            callback(time, frame);
        });

        this.force = false;
    }
}
