import type { Effect, EffectComposer, Pass, ToneMappingEffect as _TonemappingEffect } from "postprocessing";
import { HalfFloatType, NoToneMapping } from "three";

import { showBalloonWarning } from "../../engine/debug/index.js";
// import { internal_SetSharpeningEffectModule } from "./Effects/Sharpening.js";
import { MODULES } from "../../engine/engine_modules.js";
import { Context } from "../../engine/engine_setup.js";
import type { Constructor } from "../../engine/engine_types.js";
import { DeviceUtilities, getParam } from "../../engine/engine_utils.js";
import { Camera } from "../Camera.js";
import { PostProcessingEffect, PostProcessingEffectContext } from "./PostProcessingEffect.js";

declare const NEEDLE_USE_POSTPROCESSING: boolean;
globalThis["NEEDLE_USE_POSTPROCESSING"] = globalThis["NEEDLE_USE_POSTPROCESSING"] !== undefined ? globalThis["NEEDLE_USE_POSTPROCESSING"] : true;


const debug = getParam("debugpost");
const dontMergePasses = getParam("debugpostpasses");

const activeKey = Symbol("needle:postprocessing-handler");
const autoclearSetting = Symbol("needle:previous-autoclear-state");

/**
 * PostProcessingHandler is responsible for applying post processing effects to the scene. It is internally used by the {@link Volume} component
 */
export class PostProcessingHandler {

    private _composer: EffectComposer | null = null;
    private _lastVolumeComponents?: PostProcessingEffect[];
    private _effects: Array<Effect | Pass> = [];

    get isActive() {
        return this._isActive;
    }

    get composer() {
        return this._composer;
    }

    private _isActive: boolean = false;
    private readonly context: Context;

    constructor(context: Context) {
        this.context = context;
    }

    apply(components: PostProcessingEffect[]) : Promise<void> {
        if ("env" in import.meta && import.meta.env.VITE_NEEDLE_USE_POSTPROCESSING === "false") {
            if (debug) console.warn("Postprocessing is disabled via vite env setting");
            else console.debug("Postprocessing is disabled via vite env setting");
            return Promise.resolve();
        }
        if (!NEEDLE_USE_POSTPROCESSING) {
            if (debug) console.warn("Postprocessing is disabled via global vite define setting");
            else console.debug("Postprocessing is disabled via vite define");
            return Promise.resolve();
        }

        this._isActive = true;
        return this.onApply(this.context, components);
    }

    unapply() {
        if (debug) console.log("Unapplying postprocessing effects");
        this._isActive = false;
        if (this._lastVolumeComponents) {
            for (const component of this._lastVolumeComponents) {
                component.unapply();
            }
            this._lastVolumeComponents.length = 0;
        }
        const context = this.context;
        const active = context[activeKey] as PostProcessingHandler | null;
        if (active === this) {
            delete context[activeKey];
        }
        if (context.composer === this._composer) {
            context.composer?.dispose();
            context.composer = null;
        }
        if (typeof context.renderer[autoclearSetting] === "boolean") {
            context.renderer.autoClear = context.renderer[autoclearSetting];
        }
    }

    dispose() {
        this.unapply();

        for (const effect of this._effects) {
            effect.dispose();
        }
        this._effects.length = 0;
        this._composer = null;
    }

    private async onApply(context: Context, components: PostProcessingEffect[]) {

        if (!components) return;

        // IMPORTANT
        // Load postprocessing modules ONLY here to get lazy loading of the postprocessing package
        await Promise.all([
            MODULES.POSTPROCESSING.load(),
            MODULES.POSTPROCESSING_AO.load(),
            // import("./Effects/Sharpening.effect")
        ]);

        // try {
        //     internal_SetSharpeningEffectModule(modules[2]);
        // }
        // catch (err) {
        //     console.error(err);
        // }

        context[activeKey] = this;

        if (debug) console.log("Apply Postprocessing Effects", components);

        this._lastVolumeComponents = [...components];

        // store all effects in an array to apply them all in one pass
        // const effects: Array<Effect | Pass> = [];
        this._effects.length = 0;


        // TODO: if an effect is added or removed during the loop this might not be correct anymore
        const ctx: PostProcessingEffectContext = {
            handler: this,
            components: this._lastVolumeComponents,
        }
        for (let i = 0; i < this._lastVolumeComponents.length; i++) {
            const component = this._lastVolumeComponents[i];
            //@ts-ignore
            component.context = context;
            if (component.apply) {
                if (component.active) {
                    if (!context.mainCameraComponent) {
                        console.error("No camera in scene found or available yet - can not create postprocessing effects");
                        return;
                    }
                    // apply or collect effects
                    const res = component.apply(ctx);
                    if (!res) continue;
                    if (Array.isArray(res)) {
                        this._effects.push(...res);
                    }
                    else this._effects.push(res);
                }
            }
            else {
                if (component.active)
                    showBalloonWarning("Volume component is not a VolumeComponent: " + component["__type"]);
            }
        }

        // Ensure that we have a tonemapping effect if the renderer is set to use a tone mapping
        if (this.context.renderer.toneMapping != NoToneMapping) {
            if (!this._effects.find(e => e instanceof MODULES.POSTPROCESSING.MODULE.ToneMappingEffect)) {
                const tonemapping = new MODULES.POSTPROCESSING.MODULE.ToneMappingEffect();
                this._effects.push(tonemapping);
            }
        }

        this.applyEffects(context);
    }


    /** Build composer passes */
    private applyEffects(context: Context) {

        const effectsOrPasses = this._effects;
        
        if (effectsOrPasses.length <= 0) return;

        const camera = context.mainCameraComponent as Camera;
        const renderer = context.renderer;
        const scene = context.scene;
        const cam = camera.threeCamera;

        // Store the auto clear setting because the postprocessing composer just disables it
        // and when we disable postprocessing we want to restore the original setting
        // https://github.com/pmndrs/postprocessing/blob/271944b74b543a5b743a62803a167b60cc6bb4ee/src/core/EffectComposer.js#L230C12-L230C12
        renderer[autoclearSetting] = renderer.autoClear;

        // create composer and set active on context
        if (!this._composer) {
            // const hdrRenderTarget = new WebGLRenderTarget(window.innerWidth, window.innerHeight, { type: HalfFloatType });
            this._composer = new MODULES.POSTPROCESSING.MODULE.EffectComposer(renderer, {
                frameBufferType: HalfFloatType,
                stencilBuffer: true,
            });
        }

        if (context.composer && context.composer !== this._composer) {
            console.warn("There's already an active EffectComposer in your scene: replacing it with a new one. This might cause unexpected behaviour. Make sure to only use one PostprocessingManager/Volume in your scene.");
        }
        context.composer = this._composer;
        const composer = context.composer;
        composer.setMainCamera(cam);
        composer.setRenderer(renderer);
        composer.setMainScene(scene);

        for (const prev of composer.passes)
            prev.dispose();
        composer.removeAllPasses();

        // Render to screen pass
        const screenpass = new MODULES.POSTPROCESSING.MODULE.RenderPass(scene, cam);
        screenpass.name = "Render To Screen";
        screenpass.mainScene = scene;
        composer.addPass(screenpass);

        const automaticEffectsOrdering = true;
        if (automaticEffectsOrdering && !dontMergePasses) {
            try {
                this.orderEffects();

                const effects: Array<Effect> = [];

                for (const ef of effectsOrPasses) {
                    if (ef instanceof MODULES.POSTPROCESSING.MODULE.Effect)
                        effects.push(ef as Effect);
                    else if (ef instanceof MODULES.POSTPROCESSING.MODULE.Pass) {
                        composer.addPass(ef as Pass);
                    }
                    else {
                        // seems some effects are not correctly typed, but three can deal with them,
                        // so we might need to just pass them through
                        composer.addPass(ef);
                    }
                }

                // create and apply uber pass
                if (effects.length > 0) {
                    const pass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, ...effects);
                    pass.name = effects.map(e => e.name).join(" ");
                    pass.mainScene = scene;
                    pass.enabled = true;
                    composer.addPass(pass);
                }
            }
            catch (e) {
                console.error("Error while applying postprocessing effects", e);
                composer.removeAllPasses();
            }
        }
        else {
            // we still want to sort passes, but we do not want to merge them for debugging
            if (automaticEffectsOrdering)
                this.orderEffects();

            for (const ef of effectsOrPasses) {
                if (ef instanceof MODULES.POSTPROCESSING.MODULE.Effect) {
                    const pass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, ef as Effect);
                    pass.name = ef.name;
                    composer.addPass(pass);
                }
                else if (ef instanceof MODULES.POSTPROCESSING.MODULE.Pass)
                    composer.addPass(ef as Pass);
                else
                    // seems some effects are not correctly typed, but three can deal with them,
                    // so we just pass them through
                    composer.addPass(ef);
            }
        }

        if (debug) {

            console.log("[PostProcessing] Passes →", composer.passes);

            // DepthEffect for debugging purposes, disabled by default, can be selected in the debug pass select
            const depthEffect = new MODULES.POSTPROCESSING.MODULE.DepthEffect({
                blendFunction: MODULES.POSTPROCESSING.MODULE.BlendFunction.NORMAL,
                inverted: true,
            });
            depthEffect.name = "Depth Effect";
            const depthPass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, depthEffect);
            depthPass.name = "Depth Effect Pass";
            depthPass.enabled = false;
            composer.passes.push(depthPass);

            if (this._passIndices !== null) {
                const newPasses = [composer.passes[0]];
                if (this._passIndices.length > 0) {
                    newPasses.push(...this._passIndices
                        .filter(x => x !== 0)
                        .map(index => composer.passes[index])
                        .filter(pass => pass)
                    );
                }
                if (newPasses.length > 0) {
                    console.log("[PostProcessing] Passes (selected) →", newPasses);
                }
                composer.passes.length = 0;
                for (const pass of newPasses) {
                    pass.enabled = true;
                    pass.renderToScreen = false; // allows automatic setting for the last pass
                    composer.addPass(pass);
                }
            }

            const menu = this.context.menu;
            if (menu && this._passIndices === null) {
                if (this._menuEntry)
                    this._menuEntry.remove();
                
                const select = document.createElement("select");
                select.multiple = true;
                const defaultOpt = document.createElement("option");
                defaultOpt.innerText = "Final Output";
                defaultOpt.value = "-1";
                select.appendChild(defaultOpt);
                for (const eff of composer.passes) {
                    const opt = document.createElement("option");
                    opt.innerText = eff.name;
                    opt.value = `${composer.passes.indexOf(eff)}`;
                    opt.title = eff.name;
                    select.appendChild(opt);
                }
                menu.appendChild(select);
                this._menuEntry = select;
                select.addEventListener("change", () => {
                    const indices = Array.from(select.selectedOptions).map(option => parseInt(option.value));
                    if (indices.length === 1 && indices[0] === -1) {
                        this._passIndices = null;
                    }
                    else {
                        this._passIndices = indices;
                    }

                    this.applyEffects(context);
                });
            }
        }
    }

    private _menuEntry: HTMLSelectElement | null = null;
    private _passIndices: number[] | null = null;

    private orderEffects() {
        if (debug) console.log("Before ordering effects", [...this._effects]);



        // Order of effects for correct results.
        // Aligned with https://github.com/pmndrs/postprocessing/wiki/Effect-Merging#effect-execution-order
        // We can not put this into global scope because then the module might not yet be initialized
        effectsOrder ??= [
            MODULES.POSTPROCESSING.MODULE.NormalPass,
            MODULES.POSTPROCESSING.MODULE.DepthDownsamplingPass,
            MODULES.POSTPROCESSING.MODULE.SMAAEffect,
            MODULES.POSTPROCESSING.MODULE.SSAOEffect,
            MODULES.POSTPROCESSING_AO.MODULE.N8AOPostPass,
            MODULES.POSTPROCESSING.MODULE.TiltShiftEffect,
            MODULES.POSTPROCESSING.MODULE.DepthOfFieldEffect,
            MODULES.POSTPROCESSING.MODULE.ChromaticAberrationEffect,
            MODULES.POSTPROCESSING.MODULE.BloomEffect,
            MODULES.POSTPROCESSING.MODULE.SelectiveBloomEffect,
            MODULES.POSTPROCESSING.MODULE.VignetteEffect,
            MODULES.POSTPROCESSING.MODULE.PixelationEffect,
            MODULES.POSTPROCESSING.MODULE.ToneMappingEffect,
            MODULES.POSTPROCESSING.MODULE.HueSaturationEffect,
            MODULES.POSTPROCESSING.MODULE.BrightnessContrastEffect,
            // __SHARPENING_MODULE._SharpeningEffect,
        ];


        // TODO: enforce correct order of effects (e.g. DOF before Bloom)
        const effects = this._effects;
        effects.sort((a, b) => {
            // we use find index here because sometimes constructor names are prefixed with `_`
            // TODO: find a more robust solution that isnt name based (not sure if that exists tho... maybe we must give effect TYPES some priority/index)
            const aidx = effectsOrder!.findIndex(e => a.constructor.name.endsWith(e.name));
            const bidx = effectsOrder!.findIndex(e => b.constructor.name.endsWith(e.name));
            // Unknown effects should be rendered first
            if (aidx < 0) {
                if (debug) console.warn("Unknown effect found: ", a.constructor.name);
                return -1;
            }
            else if (bidx < 0) {
                if (debug) console.warn("Unknown effect found: ", b.constructor.name);
                return 1;
            }
            if (aidx < 0) return 1;
            if (bidx < 0) return -1;
            return aidx - bidx;
        });
        if (debug) console.log("After ordering effects", [...this._effects]);
        for (let i = 0; i < effects.length; i++) {
            const effect = effects[i] as any;
            if (effect?.configuration?.gammaCorrection !== undefined) {
                const isLast = i === effects.length - 1;
                effect.configuration.gammaCorrection = isLast;
            }
        }
    }
}

let effectsOrder: Array<Constructor<Effect | Pass>> | null = null;