import type { ToneMappingEffect as _TonemappingEffect, ToneMappingMode } from "postprocessing";
import { ACESFilmicToneMapping, AgXToneMapping, LinearToneMapping, NeutralToneMapping, NoToneMapping, ReinhardToneMapping } from "three";

import { MODULES } from "../../../engine/engine_modules.js";
import { serializable } from "../../../engine/engine_serialization.js";
import { getParam } from "../../../engine/engine_utils.js";
import { EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
import { findPostProcessingManager } from "../utils.js";
import { VolumeParameter } from "../VolumeParameter.js";
import { registerCustomEffectType } from "../VolumeProfile.js";

const debug = getParam("debugpost");


enum NEToneMappingMode {
    None = 0,
    Neutral = 1,            // Neutral tonemapper, close to Reinhard
    ACES = 2,               // ACES Filmic reference tonemapper (custom approximation)
    AgX = 3,                // AgX Filmic tonemapper
    KhronosNeutral = 4,     // PBR Neural tonemapper
}

type NEToneMappingModeNames = keyof typeof NEToneMappingMode;


function toThreeToneMapping(mode: NEToneMappingMode | undefined) {
    switch (mode) {
        case NEToneMappingMode.None:
            return LinearToneMapping;
        case NEToneMappingMode.Neutral:
            return ReinhardToneMapping;
        case NEToneMappingMode.ACES:
            return ACESFilmicToneMapping;
        case NEToneMappingMode.AgX:
            return AgXToneMapping;
        case NEToneMappingMode.KhronosNeutral:
            return NeutralToneMapping;
        default:
            return NeutralToneMapping;
    }
}

function threeToNeToneMapping(mode: number | undefined): NEToneMappingMode {
    switch (mode) {
        case LinearToneMapping: return NEToneMappingMode.None;
        case ACESFilmicToneMapping: return NEToneMappingMode.ACES;
        case AgXToneMapping: return NEToneMappingMode.AgX;
        case NeutralToneMapping: return NEToneMappingMode.Neutral;
        case ReinhardToneMapping: return NEToneMappingMode.Neutral;
        default: return NEToneMappingMode.None;
    }

}


function threeToneMappingToEffectMode(mode: number | undefined): ToneMappingMode {
    switch (mode) {
        case LinearToneMapping: return MODULES.POSTPROCESSING.MODULE.ToneMappingMode.LINEAR;
        case ACESFilmicToneMapping: return MODULES.POSTPROCESSING.MODULE.ToneMappingMode.ACES_FILMIC;
        case AgXToneMapping: return MODULES.POSTPROCESSING.MODULE.ToneMappingMode.AGX;
        case NeutralToneMapping: return MODULES.POSTPROCESSING.MODULE.ToneMappingMode.NEUTRAL;
        case ReinhardToneMapping: return MODULES.POSTPROCESSING.MODULE.ToneMappingMode.REINHARD;
        default: return MODULES.POSTPROCESSING.MODULE.ToneMappingMode.LINEAR;
    }
}

/**
 * @category Effects
 * @group Components
 */
export class ToneMappingEffect extends PostProcessingEffect {

    get typeName() {
        return "ToneMapping";
    }

    @serializable(VolumeParameter)
    readonly mode: VolumeParameter = new VolumeParameter(undefined);

    @serializable(VolumeParameter)
    readonly exposure: VolumeParameter = new VolumeParameter(1);

    /** Set the tonemapping mode to e.g. "agx" */
    setMode(mode: NEToneMappingModeNames) {
        const enumValue = NEToneMappingMode[mode as NEToneMappingModeNames];
        if (enumValue === undefined) {
            console.error("Invalid ToneMapping mode", mode);
            return this;
        }
        this.mode.value = enumValue;
        return this;
    }

    get isToneMapping() { return true; }

    onEffectEnabled(): void {
        // Tonemapping works with and without a postprocessing manager. 
        // If there's no manager already in the scene we don't need to create one because tonemapping can also be applied without a postprocessing pass
        const ppmanager = findPostProcessingManager(this);
        if (!ppmanager) return;
        super.onEffectEnabled(ppmanager);
    }

    onCreateEffect(): EffectProviderResult | undefined {

        // TODO: this should be done in the PostProcessingHandler
        if (this.postprocessingContext) {
            for (const other of this.postprocessingContext.components) {
                // If we're the first tonemapping effect it's all good
                if (other === this) break;
                // If another tonemapping effect is found, warn the user
                if (other != this && other instanceof ToneMappingEffect) {
                    console.warn("Multiple tonemapping effects found in the same postprocessing stack: Please check your scene setup.", { activeEffect: other, ignoredEffect: this });
                    return undefined;
                }
            }
        }


        // ensure the effect tonemapping value is initialized
        if (this.mode.isInitialized == false) {
            const init = threeToNeToneMapping(this.context.renderer.toneMapping);
            this.mode.initialize(init);
        }

        const threeMode = toThreeToneMapping(this.mode.value);
        const tonemapping = new MODULES.POSTPROCESSING.MODULE.ToneMappingEffect({
            mode: threeToneMappingToEffectMode(threeMode),
        });
        this.mode.onValueChanged = (newValue) => {
            const threeMode = toThreeToneMapping(newValue);
            tonemapping.mode = threeToneMappingToEffectMode(threeMode);
            if (debug) console.log("ToneMapping mode changed to", NEToneMappingMode[newValue], threeMode, tonemapping.mode);
        };
        if (debug) console.log("Use ToneMapping", NEToneMappingMode[this.mode.value], threeMode, tonemapping.mode, "renderer.tonemapping: " + this.context.renderer.toneMapping);


        this.exposure.onValueChanged = (newValue) => {
            this.context.renderer.toneMappingExposure = newValue;
        };

        return tonemapping;
    }


    onBeforeRender(): void {
        if (this.mode.overrideState)
            this.context.renderer.toneMapping = toThreeToneMapping(this.mode.value);
        if (this.exposure.overrideState && this.exposure.value !== undefined)
            this.context.renderer.toneMappingExposure = this.exposure.value
    }


}

registerCustomEffectType("Tonemapping", ToneMappingEffect);