import type { Effect } from "postprocessing";

import { showBalloonMessage } from "../../engine/debug/index.js";
import type { EditorModification, IEditorModification as IEditorModificationReceiver } from "../../engine/engine_editor-sync.js";
import { serializeable } from "../../engine/engine_serialization_decorator.js";
import { getParam } from "../../engine/engine_utils.js";
import type { PostProcessing } from "../../engine/postprocessing/index.js";
import { Behaviour } from "../Component.js";
import { EffectWrapper } from "./Effects/EffectWrapper.js";
import { PostProcessingEffect } from "./PostProcessingEffect.js";
import { IPostProcessingManager, setPostprocessingManagerType } from "./utils.js";
import { VolumeParameter } from "./VolumeParameter.js";
import { VolumeProfile } from "./VolumeProfile.js";

const debug = getParam("debugpost");


/** [Volume](https://engine.needle.tools/docs/api/Volume) The Volume/PostprocessingManager component is responsible for managing post processing effects.
 * Add this component to any object in your scene to enable post processing effects.
 *
 * Effects added to this Volume (via profile or code) are pushed to `context.postprocessing` when the Volume is enabled,
 * and removed when it is disabled.
 *
 * @example Add bloom
 * ```ts
 * const volume = new Volume();
 * volume.addEffect(new BloomEffect({
 *   intensity: 3,
 *   luminanceThreshold: .2
 * }));
 * gameObject.addComponent(volume);
 * ```
 *
 * @example Remove bloom
 * ```ts
 * volume.removeEffect(bloom);
 * ```
 *
 * @example Add pixelation
 * ```ts
 * const pixelation = new PixelationEffect();
 * pixelation.granularity.value = 10;
 * volume.addEffect(pixelation);
 * ```
 *
 * @summary Manage Post-Processing Effects
 * @category Rendering
 * @category Effects
 * @see {@link VolumeProfile} for profile-based effect management
 * @see {@link PostProcessingEffect} for creating custom effects
 * @see {@link PostProcessing} for core Needle Engine postprocessing control, also accessible via `context.postprocessing`
 * @group Components
 */
export class Volume extends Behaviour implements IEditorModificationReceiver, IPostProcessingManager {

    get isPostProcessingManager() {
        return true;
    }

    /** Currently active postprocessing effects managed by this Volume */
    get effects() {
        return this._activeEffects;
    }

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

    @serializeable(VolumeProfile)
    sharedProfile?: VolumeProfile;

    /**
     * Set multisampling to "auto" to automatically adjust the multisampling level based on performance.
     * Set to a number to manually set the multisampling level.
     * Pushed to `context.postprocessing.multisampling` when this Volume is active.
     * @default "auto"
     * @min 0
     * @max renderer.capabilities.maxSamples
     */
    @serializeable()
    multisampling: "auto" | number = "auto";

    /** When enabled, the device pixel ratio will be gradually reduced when FPS is low
     * and restored when performance recovers. This helps maintain smooth frame rates
     * on devices where full retina resolution is too expensive for postprocessing.
     * Pushed to `context.postprocessing.adaptiveResolution` when this Volume is active.
     * Disable this if you need a fixed resolution and prefer consistent quality over frame rate.
     * @default true
     */
    @serializeable()
    adaptiveResolution: boolean = true;

    /**
     * Add a post processing effect to this Volume and push it to the core postprocessing stack.
     */
    addEffect<T extends PostProcessingEffect | Effect>(effect: T & { order?: number }): T {
        let entry = effect as PostProcessingEffect;
        if (!(entry instanceof PostProcessingEffect)) {
            entry = new EffectWrapper(entry);
            if (typeof effect.order === "number") entry.order = effect.order;
        }
        if (entry.gameObject === undefined) this.gameObject.addComponent(entry);
        if (this._effects.includes(entry)) return effect;
        this._effects.push(entry);
        // Track as active so removeEffectsFromCore cleans it up on disable
        if (!this._activeEffects.includes(entry))
            this._activeEffects.push(entry);
        // Push to core stack
        this.context.postprocessing.addEffect(entry);
        this._isDirty = true;
        return effect;
    }

    /**
     * Remove a post processing effect from this Volume and the core postprocessing stack.
     */
    removeEffect<T extends PostProcessingEffect | Effect>(effect: T): T {

        let index = -1;
        let entry: PostProcessingEffect | undefined;
        if (!(effect instanceof PostProcessingEffect)) {
            index = this._effects.findIndex(e => e instanceof EffectWrapper && e.effect === effect);
            if (index !== -1) entry = this._effects[index];
        }
        else {
            index = this._effects.indexOf(effect);
            if (index !== -1) entry = this._effects[index];
        }

        if (index !== -1 && entry) {
            this._effects.splice(index, 1);
            this.context.postprocessing.removeEffect(entry);
            this._isDirty = true;
            return effect;
        }
        else if (effect instanceof PostProcessingEffect) {
            // if the effect is part of the shared profile remove it from there
            const si = this.sharedProfile?.components?.indexOf(effect);
            if (si !== undefined && si !== -1) {
                this.sharedProfile?.components?.splice(si, 1);
                this.context.postprocessing.removeEffect(effect);
                this._isDirty = true;
            }
        }

        return effect;
    }

    private readonly _activeEffects: PostProcessingEffect[] = [];
    private readonly _effects: PostProcessingEffect[] = [];

    /**
     * When dirty the post processing effects will be re-applied
     */
    markDirty(): void {
        this._isDirty = true;
    }

    /** @internal */
    awake() {
        if (debug) {
            console.log("PostprocessingManager Awake", this);
            console.log("Press P to toggle post processing");
            window.addEventListener("keydown", (e) => {
                if (e.key === "p") {
                    this.enabled = !this.enabled;
                    showBalloonMessage("Toggle PostProcessing " + this.name + ": Enabled=" + this.enabled);
                    this.markDirty();
                }
            });
        }

        // ensure the profile is initialized
        this.sharedProfile?.__init(this);
    }

    private _isDirty: boolean = false;

    /** @internal */
    onEnable(): void {
        this._isDirty = true;
        this.pushEffectsToCore();
        this.syncConfigToCore();
    }

    /** @internal */
    onDisable() {
        this.removeEffectsFromCore();
        this._isDirty = false;
    }

    /** @internal */
    onBeforeRender(): void {
        if (this._isDirty) {
            this.removeEffectsFromCore();
            this.pushEffectsToCore();
            this._isDirty = false;
        }
        // Push config changes (user may change multisampling/adaptiveResolution at runtime)
        this.syncConfigToCore();

        // Process queued editor modifications after the handler is ready
        if (this.context.postprocessing.handler) {
            this._applyPostQueue();
        }
    }

    /** @internal */
    onDestroy(): void {
        this.removeEffectsFromCore();
    }

    /** Collect active effects from profile + code and push them to context.postprocessing */
    private pushEffectsToCore() {
        this._activeEffects.length = 0;

        // get from profile
        if (this.sharedProfile?.components) {
            const comps = this.sharedProfile.components;
            for (const effect of comps) {
                if (effect.active && effect.enabled && !this._activeEffects.includes(effect))
                    this._activeEffects.push(effect);
            }
        }
        // add effects registered via code
        for (const effect of this._effects) {
            if (effect.active && effect.enabled && !this._activeEffects.includes(effect))
                this._activeEffects.push(effect);
        }

        const pp = this.context.postprocessing;
        for (const effect of this._activeEffects) {
            pp.addEffect(effect);
        }
    }

    /** Remove all effects this Volume contributed from the core stack */
    private removeEffectsFromCore() {
        const pp = this.context.postprocessing;
        for (const effect of this._activeEffects) {
            pp.removeEffect(effect);
        }
        this._activeEffects.length = 0;
    }

    /** Push multisampling and adaptiveResolution config to the core stack */
    private syncConfigToCore() {
        const pp = this.context.postprocessing;
        pp.multisampling = this.multisampling;
        pp.adaptiveResolution = this.adaptiveResolution;
    }

    private _applyPostQueue() {
        if (this._modificationQueue) {
            for (const entry of this._modificationQueue.values()) this.onEditorModification(entry);
            this._modificationQueue.clear();
        }
    }

    /** called from needle editor sync package if its active */
    onEditorModification(modification: EditorModification): void | boolean | undefined {

        if (modification.propertyName.startsWith("postprocessing.")) {

            if (!this.context.postprocessing.handler) {
                if (!this._modificationQueue) this._modificationQueue = new Map<string, EditorModification>();
                this._modificationQueue.set(modification.propertyName, modification);
                return true;
            }

            if (!this._activeEffects?.length) return;
            const path = modification.propertyName.split(".");
            if (path.length === 3 || path.length === 4) {
                const componentName = path[1];
                const propertyName = path[2];
                for (const comp of this._activeEffects) {
                    if (comp.typeName?.toLowerCase() === componentName.toLowerCase()) {

                        if (propertyName === "active") {
                            comp.active = modification.value;
                            this.scheduleRecreate();
                            return;
                        }

                        // cache the volume parameters
                        if (!effectVolumeProperties.has(componentName)) {
                            const volumeParameterKeys = new Array<string>();
                            effectVolumeProperties.set(componentName, volumeParameterKeys);
                            const keys = Object.keys(comp);
                            for (const key of keys) {
                                const prop = comp[key];
                                if (prop instanceof VolumeParameter) {
                                    volumeParameterKeys.push(key);
                                }
                            }
                        }

                        if (effectVolumeProperties.has(componentName)) {
                            const paramName = propertyName.toLowerCase();
                            const volumeParameterKeys = effectVolumeProperties.get(componentName)!;
                            for (const key of volumeParameterKeys) {
                                if (key.toLowerCase() === paramName) {
                                    const prop = comp[key] as VolumeParameter;
                                    if (prop instanceof VolumeParameter) {
                                        const isActiveStateChange = path.length === 4 && path[3] === "active";
                                        if (isActiveStateChange) {
                                            prop.overrideState = modification.value;
                                            this.scheduleRecreate();
                                        }
                                        else if (prop && prop.value !== undefined) {
                                            prop.value = modification.value;
                                        }
                                    }
                                    return;
                                }
                            }
                        }

                        console.warn("Unknown modification", propertyName);
                        return;
                    }
                }
            }
            return true;
        }

        return false;
    }

    private _modificationQueue?: Map<string, EditorModification>;

    private _recreateId: number = -1;
    private scheduleRecreate() {
        // When the editor modifications come in with changed active effects we want/need to re-create the effects
        // We defer it slightly because multiple active changes could be made and we dont want to recreate the full effect stack multiple times
        const id = ++this._recreateId;
        setTimeout(() => {
            if (id !== this._recreateId) return;
            this.onDisable();
            this.onEnable();
        }, 200);
    }

}

/** cached VolumeParameter keys per object */
const effectVolumeProperties: Map<string, string[]> = new Map<string, string[]>();


setPostprocessingManagerType(Volume);

export { Volume as PostProcessingManager };
