import { N8AOPostPass } from "n8ao";
import {
    BloomEffect, BrightnessContrastEffect, ChromaticAberrationEffect, 
    DepthDownsamplingPass, DepthOfFieldEffect, 
    Effect, EffectComposer, EffectPass, HueSaturationEffect, 
    NormalPass, Pass, 
    PixelationEffect, RenderPass, 
    SelectiveBloomEffect, SMAAEffect, SSAOEffect,
    TiltShiftEffect,
    ToneMappingEffect as _TonemappingEffect, VignetteEffect} from "postprocessing";
import { HalfFloatType, NoToneMapping } from "three";

import { showBalloonWarning } from "../../engine/debug/index.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 { _SharpeningEffect } from "./Effects/Sharpening.js";
import { PostProcessingEffect, PostProcessingEffectContext } from "./PostProcessingEffect.js";

const debug = getParam("debugpost");

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[]) {
        this._isActive = true;
        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 onApply(context: Context, components: PostProcessingEffect[]) {

        if (!components) return;

        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 _TonemappingEffect)) {
                const tonemapping = new _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;

        const maxSamples = renderer.capabilities.maxSamples;
        // create composer and set active on context
        if (!this._composer) {
            // const hdrRenderTarget = new WebGLRenderTarget(window.innerWidth, window.innerHeight, { type: HalfFloatType });
            this._composer = new EffectComposer(renderer, {
                frameBufferType: HalfFloatType,
                stencilBuffer: true,
                multisampling: Math.min(DeviceUtilities.isMobileDevice() ? 4 : 8, maxSamples),
            });
        }
        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 RenderPass(scene, cam);
        screenpass.name = "Render To Screen";
        screenpass.mainScene = scene;
        composer.addPass(screenpass);

        const automaticEffectsOrdering = true;
        if (automaticEffectsOrdering) {
            try {
                this.orderEffects();

                const effects: Array<Effect> = [];

                for (const ef of effectsOrPasses) {
                    if (ef instanceof Effect)
                        effects.push(ef as Effect);
                    else if (ef instanceof Pass) {
                        const pass = new EffectPass(cam, ...effects);
                        pass.mainScene = scene;
                        pass.name = effects.map(e => e.constructor.name).join(", ");
                        pass.enabled = true;
                        // composer.addPass(pass);
                        effects.length = 0;
                        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 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 {
            for (const ef of effectsOrPasses) {
                if (ef instanceof Effect)
                    composer.addPass(new EffectPass(cam, ef as Effect));
                else if (ef instanceof 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", effectsOrPasses, "->", composer.passes);
    }

    private orderEffects() {
        if (debug) console.log("Before ordering effects", [...this._effects]);
        // 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;
            }
        }
    }
}

// Order of effects for correct results.
// Aligned with https://github.com/pmndrs/postprocessing/wiki/Effect-Merging#effect-execution-order
export const effectsOrder: Array<Constructor<Effect | Pass>> = [
    NormalPass,
    DepthDownsamplingPass,
    SMAAEffect,
    SSAOEffect,
    N8AOPostPass,
    TiltShiftEffect,
    DepthOfFieldEffect,
    ChromaticAberrationEffect,
    BloomEffect,
    SelectiveBloomEffect,
    VignetteEffect,
    PixelationEffect,
    _TonemappingEffect,
    HueSaturationEffect,
    BrightnessContrastEffect,
    _SharpeningEffect,
];