import type { EffectComposer } from "postprocessing";
import type { ToneMapping } from "three";
import type { EffectComposer as ThreeEffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";

import { isDevEnvironment } from "../debug/index.js";
import type { Context } from "../engine_context.js";
import { DeviceUtilities, getParam } from "../engine_utils.js";
import type { IPostProcessingEffect, IPostProcessingHandler, ITonemappingEffect } from "./types.js";

const debug = getParam("debugpost");

/**
 * Core postprocessing stack accessible via `context.postprocessing`.
 * Manages the effect pipeline independently of any specific component.
 *
 * Volumes and individual PostProcessingEffect components add/remove effects
 * to this stack. The stack builds the EffectComposer pipeline when dirty.
 *
 * @example Add an effect directly
 * ```ts
 * const bloom = new BloomEffect({ intensity: 3 });
 * this.context.postprocessing.addEffect(bloom);
 * ```
 *
 * @example Remove an effect
 * ```ts
 * this.context.postprocessing.removeEffect(bloom);
 * ```
 */
export class PostProcessing {

    private readonly _context: Context;
    private _handler: IPostProcessingHandler | null = null;
    private readonly _effects: IPostProcessingEffect[] = [];
    private _isDirty: boolean = false;

    /** Currently active postprocessing effects in the stack */
    get effects(): readonly IPostProcessingEffect[] {
        return this._effects;
    }

    get dirty() { return this._isDirty; }
    set dirty(value: boolean) { this._isDirty = value; }

    /** The internal PostProcessingHandler that manages the EffectComposer pipeline */
    get handler(): IPostProcessingHandler | null { return this._handler; }

    /**
     * The effect composer used to render postprocessing effects.
     * This is set internally by the PostProcessingHandler when effects are applied.
     */
    get composer(): EffectComposer | ThreeEffectComposer | null { return this._composer; }
    set composer(value: EffectComposer | ThreeEffectComposer | null) { this._composer = value; }
    private _composer: EffectComposer | ThreeEffectComposer | null = null;

    /**
     * Set multisampling to "auto" to automatically adjust the multisampling level based on performance.
     * Set to a number to manually set the multisampling level.
     * @default "auto"
     */
    multisampling: "auto" | number = "auto";

    /** When enabled, the device pixel ratio will be gradually reduced when FPS is low
     * and restored when performance recovers.
     * @default true
     */
    adaptiveResolution: boolean = true;

    constructor(context: Context) {
        this._context = context;
    }

    /**
     * Add a post processing effect to the stack.
     * The effect stack will be rebuilt on the next update.
     */
    addEffect(effect: IPostProcessingEffect): void {
        if (this._effects.includes(effect)) return;
        this._effects.push(effect);
        this._isDirty = true;
    }

    /**
     * Remove a post processing effect from the stack.
     * The effect stack will be rebuilt on the next update.
     */
    removeEffect(effect: IPostProcessingEffect): void {
        const index = this._effects.indexOf(effect);
        if (index !== -1) {
            this._effects.splice(index, 1);
            this._isDirty = true;
        }
    }

    /** Mark the stack as dirty so the effects are rebuilt on the next update */
    markDirty(): void {
        this._isDirty = true;
    }

    // --- Adaptive multisampling state ---
    private _enabledTime: number = -1;
    private _multisampleAutoChangeTime: number = 0;
    private _multisampleAutoDecreaseTime: number = 0;

    /** @internal Called from the context render loop to update the postprocessing pipeline */
    update(): void {
        const context = this._context;
        if (context.isInXR) return;

        // Wait for a camera before applying
        if (this._isDirty && context.mainCamera) {
            this.apply();
        }

        // In tonemapping-only mode, keep renderer values in sync with the active effect
        if (this._tonemappingOnlyActive) {
            const activeEffects = this._effects.filter(e => e.active && e.enabled && e.isToneMapping === true);
            if (activeEffects.length > 0) {
                const effect = activeEffects[activeEffects.length - 1] as ITonemappingEffect;
                context.renderer.toneMapping = effect.threeToneMapping;
                context.renderer.toneMappingExposure = effect.toneMappingExposure;
            }
            return;
        }

        if (!this._handler || !this._composer || this._handler.composer !== this._composer) return;

        // The composer is always a pmndrs EffectComposer (created by PostProcessingHandler)
        const composer = this._composer as EffectComposer;

        // Handle context lost
        if (context.renderer.getContext().isContextLost()) {
            context.renderer.forceContextRestore();
        }
        if (composer.getRenderer() !== context.renderer)
            composer.setRenderer(context.renderer);

        composer.setMainScene(context.scene);

        // --- Adaptive multisampling ---
        if (this.multisampling === "auto") {
            if (this._handler.hasSmaaEffect) {
                if (this._handler.multisampling !== 0) {
                    this._handler.multisampling = 0;
                    if (debug || isDevEnvironment()) {
                        console.log(`[PostProcessing] multisampling is disabled because it's set to 'auto' and there is an SMAA effect.\n\nIf you need multisampling consider changing 'auto' to a fixed value (e.g. 4).`);
                    }
                }
            }
            else {
                const timeSinceLastChange = context.time.realtimeSinceStartup - this._multisampleAutoChangeTime;

                if (context.time.realtimeSinceStartup - this._enabledTime > 2
                    && timeSinceLastChange > .5
                ) {
                    const prev = this._handler.multisampling;

                    if (this._handler.multisampling > 0 && context.time.smoothedFps <= 50) {
                        this._multisampleAutoChangeTime = context.time.realtimeSinceStartup;
                        this._multisampleAutoDecreaseTime = context.time.realtimeSinceStartup;
                        let newMultiSample = this._handler.multisampling * .5;
                        newMultiSample = Math.floor(newMultiSample);
                        if (newMultiSample != this._handler.multisampling) {
                            this._handler.multisampling = newMultiSample;
                        }
                        if (debug) console.debug(`[PostProcessing] Reduced multisampling from ${prev} to ${this._handler.multisampling}`);
                    }
                    else if (timeSinceLastChange > 1
                        && context.time.smoothedFps >= 59
                        && this._handler.multisampling < context.renderer.capabilities.maxSamples
                        && context.time.realtimeSinceStartup - this._multisampleAutoDecreaseTime > 10
                    ) {
                        this._multisampleAutoChangeTime = context.time.realtimeSinceStartup;
                        let newMultiSample = this._handler.multisampling <= 0 ? 1 : this._handler.multisampling * 2;
                        newMultiSample = Math.floor(newMultiSample);
                        if (newMultiSample !== this._handler.multisampling) {
                            this._handler.multisampling = newMultiSample;
                        }
                        if (debug) console.debug(`[PostProcessing] Increased multisampling from ${prev} to ${this._handler.multisampling}`);
                    }
                }
            }
        }
        else {
            const newMultiSample = Math.max(0, Math.min(this.multisampling as number, context.renderer.capabilities.maxSamples));
            if (newMultiSample !== this._handler.multisampling)
                this._handler.multisampling = newMultiSample;
        }

        // --- Adaptive pixel ratio ---
        this._handler.adaptivePixelRatio = this.adaptiveResolution;
        this._handler.updateAdaptivePixelRatio();

        // Update camera on passes if needed
        if (context.mainCamera) {
            const passes = composer.passes;
            for (const pass of passes) {
                if (pass.mainCamera && pass.mainCamera !== context.mainCamera) {
                    composer.setMainCamera(context.mainCamera);
                    break;
                }
            }
        }
    }

    private _lastApplyTime?: number;
    private _rapidApplyCount = 0;

    // --- Tonemapping-only state ---
    /** When true, tonemapping is applied directly to the renderer (no full pipeline) */
    private _tonemappingOnlyActive = false;
    private _previousToneMapping?: ToneMapping;
    private _previousToneMappingExposure?: number;

    private apply() {
        if (debug) console.log(`[PostProcessing] Apply stack (${this._effects.length} effects)`);

        if (isDevEnvironment()) {
            if (this._lastApplyTime !== undefined && Date.now() - this._lastApplyTime < 100) {
                this._rapidApplyCount++;
                if (this._rapidApplyCount === 5)
                    console.warn("[PostProcessing] Detected rapid post processing modifications - this might be a bug");
            }
            this._lastApplyTime = Date.now();
        }

        this._isDirty = false;

        // Collect active effects
        const activeEffects = this._effects.filter(e => e.active && e.enabled);

        if (activeEffects.length <= 0) {
            this.restoreTonemapping();
            this._handler?.unapply(false);
            return;
        }

        // Check if ALL active effects are tonemapping-only
        const allToneMapping = activeEffects.every(e => e.isToneMapping === true);

        if (allToneMapping) {
            // Use the last tonemapping effect added (last in the array)
            const tonemappingEffect = activeEffects[activeEffects.length - 1] as ITonemappingEffect;

            if (debug) console.log(`[PostProcessing] Only tonemapping effects in stack — applying directly to renderer`);

            // Store previous values on first activation
            if (!this._tonemappingOnlyActive) {
                this._previousToneMapping = this._context.renderer.toneMapping as ToneMapping;
                this._previousToneMappingExposure = this._context.renderer.toneMappingExposure;
                this._tonemappingOnlyActive = true;
            }

            // Apply tonemapping directly to renderer
            this._context.renderer.toneMapping = tonemappingEffect.threeToneMapping;
            this._context.renderer.toneMappingExposure = tonemappingEffect.toneMappingExposure;

            // Tear down any existing postprocessing pipeline
            this._handler?.unapply(false);
            return;
        }

        // We have non-tonemapping effects — restore renderer tonemapping if we were in tonemapping-only mode
        this.restoreTonemapping();

        // Build full postprocessing pipeline
        this.ensureHandler()
            .then(handler => {
                if (!handler) return;
                return handler.apply(activeEffects) as Promise<void> | void;
            })
            .then(() => {
                if (this._handler) {
                    if (this.multisampling === "auto") {
                        this._handler.multisampling = DeviceUtilities.isMobileDevice()
                            ? 2
                            : 4;
                    }
                    else {
                        this._handler.multisampling = Math.max(0, Math.min(this.multisampling as number, this._context.renderer.capabilities.maxSamples));
                    }
                    if (debug) console.debug(`[PostProcessing] Set multisampling to ${this._handler.multisampling} (Is Mobile: ${DeviceUtilities.isMobileDevice()})`);
                }
            });

        this._enabledTime = this._context.time.realtimeSinceStartup;
    }

    /** Restore renderer tonemapping to previous values when leaving tonemapping-only mode */
    private restoreTonemapping() {
        if (this._tonemappingOnlyActive) {
            if (this._previousToneMapping !== undefined)
                this._context.renderer.toneMapping = this._previousToneMapping;
            if (this._previousToneMappingExposure !== undefined)
                this._context.renderer.toneMappingExposure = this._previousToneMappingExposure;
            this._tonemappingOnlyActive = false;
            this._previousToneMapping = undefined;
            this._previousToneMappingExposure = undefined;
            if (debug) console.log(`[PostProcessing] Restored renderer tonemapping`);
        }
    }

    /** Lazily creates the PostProcessingHandler to avoid loading the postprocessing library until actually needed */
    private async ensureHandler(): Promise<IPostProcessingHandler> {
        if (!this._handler) {
            const { PostProcessingHandler } = await import("../../engine-components/postprocessing/PostProcessingHandler.js");
            if (!this._handler) {
                this._handler = new PostProcessingHandler(this._context);
            }
        }
        return this._handler;
    }

    /** @internal */
    dispose() {
        this.restoreTonemapping();
        this._handler?.dispose();
        this._handler = null;
        this._composer = null;
        this._effects.length = 0;
    }
}
