
import {
    AlwaysStencilFunc,
    DecrementStencilOp,
    DecrementWrapStencilOp,
    EqualStencilFunc,
    GreaterEqualStencilFunc,
    GreaterStencilFunc,
    IncrementStencilOp,
    IncrementWrapStencilOp,
    InvertStencilOp,
    KeepStencilOp,
    LessEqualStencilFunc,
    LessStencilFunc,
    // stencil funcs
    NeverStencilFunc,
    NotEqualStencilFunc,
    ReplaceStencilOp,
    type StencilFunc,
    type StencilOp as ThreeStencilOp,
    // stencil ops
    ZeroStencilOp,
} from "three";
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";

import { showBalloonWarning } from "../debug/index.js";
import { isUsingInstancing } from "../engine_gameobject.js";
import { isLocalNetwork } from "../engine_networking_utils.js";
import { type SourceIdentifier } from "../engine_types.js";
import { type IComponent as Component, type IRenderer } from "../engine_types.js";
import { getParam } from "../engine_utils.js";

const debug = getParam("debugstencil");

function matchesLayer(stencilLayer: number, comp: Component): boolean {
    return (stencilLayer & 1 << comp.layer) != 0;
}


declare type StencilSettingsModel = {
    name: string;
    event: number;
    index: number;
    queue: number;
    layer: number;
    value: number;
    compareFunc: number;
    passOp: number;
    failOp: number;
    zFailOp: number;
}

const $stencils = Symbol("stencils");

export class NEEDLE_render_objects implements GLTFLoaderPlugin {

    public get name() { return "NEEDLE_render_objects"; }

    private static stencils: { [key: string]: StencilSettingsModel[] } = {};

    static applyStencil(obj?: IRenderer | null) {
        if (!obj) return;
        const source = obj.sourceId;
        if (debug) console.log(source, NEEDLE_render_objects.stencils);
        if (!source) return;
        const settings = NEEDLE_render_objects.stencils[source];
        if (!settings) return;
        for (let i = settings.length - 1; i >= 0; i--) {
            const stencil: StencilSettingsModel = settings[i];
            if (matchesLayer(stencil.layer, obj)) {
                if (debug) console.log(stencil);
                setTimeout(() => {
                    if (isLocalNetwork() && isUsingInstancing(obj.gameObject)) {
                        showBalloonWarning("Stencil not supported on instanced objects");
                        console.warn("Stencil not supported on instanced objects", obj);
                    }
                }, 500)
                for (let i = 0; i < obj.sharedMaterials.length; i++) {
                    let mat = obj.sharedMaterials[i];
                    if (mat) {
                        // if (!mat[$stencils]) 
                        mat = mat.clone();
                        mat[$stencils] = true;
                        mat.stencilWrite = true;
                        mat.stencilWriteMask = 255;
                        mat.stencilFuncMask = 255;
                        mat.stencilRef = stencil.value;
                        mat.stencilFunc = stencil.compareFunc as StencilFunc;
                        mat.stencilZPass = stencil.passOp as ThreeStencilOp;
                        mat.stencilFail = stencil.failOp as ThreeStencilOp;
                        mat.stencilZFail = stencil.zFailOp as ThreeStencilOp;
                        obj.sharedMaterials[i] = mat;
                    }
                }
                // you can have 50 renderer features per event until this breaks
                obj.gameObject.renderOrder = stencil.event * 1000 + stencil.index * 50;
                break;
            }
        }
    }


    private parser: GLTFParser;
    private source: SourceIdentifier;

    constructor(parser: GLTFParser, source: SourceIdentifier) {
        this.parser = parser;
        this.source = source;
    }

    afterRoot(_result: GLTF): Promise<void> | null {
        const extensions = this.parser.json.extensions;
        if (extensions) {
            const ext = extensions[EXTENSION_NAME];
            if (ext) {
                if (debug)
                    console.log(ext);
                const stencils = ext.stencil;
                if (stencils && Array.isArray(stencils)) {
                    for (const stencil of stencils) {
                        const obj: StencilSettingsModel = { ...stencil };
                        obj.compareFunc = ToThreeCompareFunction(obj.compareFunc as number);
                        obj.passOp = ToThreeStencilOp(obj.passOp);
                        obj.failOp = ToThreeStencilOp(obj.failOp);
                        obj.zFailOp = ToThreeStencilOp(obj.zFailOp);

                        if (!NEEDLE_render_objects.stencils[this.source])
                            NEEDLE_render_objects.stencils[this.source] = [];
                        NEEDLE_render_objects.stencils[this.source].push(obj);
                    }
                }
            }
        }

        return null;
    }

}

enum StencilOp {
    Keep = 0,
    Zero = 1,
    Replace = 2,
    IncrementSaturate = 3,
    DecrementSaturate = 4,
    Invert = 5,
    IncrementWrap = 6,
    DecrementWrap = 7,
}

enum CompareFunction {
    Disabled,
    Never,
    Less,
    Equal,
    LessEqual,
    Greater,
    NotEqual,
    GreaterEqual,
    Always,
}

function ToThreeStencilOp(op: StencilOp): ThreeStencilOp {
    switch (op) {
        case StencilOp.Keep:
            return KeepStencilOp;
        case StencilOp.Zero:
            return ZeroStencilOp;
        case StencilOp.Replace:
            return ReplaceStencilOp;
        case StencilOp.IncrementSaturate:
            return IncrementStencilOp;
        case StencilOp.DecrementSaturate:
            return DecrementStencilOp;
        case StencilOp.IncrementWrap:
            return IncrementWrapStencilOp;
        case StencilOp.DecrementWrap:
            return DecrementWrapStencilOp;
        case StencilOp.Invert:
            return InvertStencilOp;
    }
    return 0;
}

function ToThreeCompareFunction(func: CompareFunction): StencilFunc {
    switch (func) {
        case CompareFunction.Never:
            return NeverStencilFunc;
        case CompareFunction.Less:
            return LessStencilFunc;
        case CompareFunction.Equal:
            return EqualStencilFunc;
        case CompareFunction.LessEqual:
            return LessEqualStencilFunc;
        case CompareFunction.Greater:
            return GreaterStencilFunc;
        case CompareFunction.NotEqual:
            return NotEqualStencilFunc;
        case CompareFunction.GreaterEqual:
            return GreaterEqualStencilFunc;
        case CompareFunction.Always:
            return AlwaysStencilFunc;
    }
    return NeverStencilFunc;
}


export const EXTENSION_NAME = "NEEDLE_render_objects";

