/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module WebGL
 */
import { assert, expectDefined } from "@itwin/core-bentley";
import { DebugShaderFile } from "../RenderSystemDebugControl";
import { ShaderProgramParams } from "./DrawCommand";
import { GL } from "./GL";
import { UniformHandle } from "./UniformHandle";
import { System } from "./System";
import { TechniqueFlags } from "./TechniqueFlags";
/** Describes the location of a uniform variable within a shader program.
 * @internal
 */
export class Uniform {
    _name;
    _handle;
    constructor(name) { this._name = name; }
    compile(prog) {
        assert(!this.isValid);
        if (undefined !== prog.glProgram) {
            this._handle = UniformHandle.create(prog, this._name);
        }
        return this.isValid;
    }
    get isValid() { return undefined !== this._handle; }
}
/**
 * Describes the location of a uniform variable within a shader program, the value of which does not change while the program is active.
 * The supplied binding function will be invoked once each time the shader becomes active to set the value of the uniform.
 * @internal
 */
export class ProgramUniform extends Uniform {
    _bind;
    constructor(name, bind) {
        super(name);
        this._bind = bind;
    }
    bind(params) {
        if (undefined !== this._handle) {
            this._bind(this._handle, params);
        }
    }
}
/**
 * Describes the location of a uniform variable within a shader program, the value of which is dependent upon the graphic primitive
 * currently being rendered by the program. The supplied binding function will be invoked once for each graphic primitive submitted
 * to the program to set the value of the uniform.
 * @internal
 */
export class GraphicUniform extends Uniform {
    _bind;
    constructor(name, bind) {
        super(name);
        this._bind = bind;
    }
    bind(params) {
        if (undefined !== this._handle) {
            this._bind(this._handle, params);
        }
    }
}
/** @internal */
export class ShaderProgram {
    vertSource;
    fragSource;
    _glProgram;
    _inUse = false;
    _status = 2 /* CompileStatus.Uncompiled */;
    _programUniforms = new Array();
    _graphicUniforms = new Array();
    _attrMap;
    // for debugging purposes...
    description;
    _fragDescription;
    _vertGNdx = -1;
    _fragGNdx = -1;
    _vertHNdx = -1;
    _fragHNdx = -1;
    outputsToPick;
    constructor(gl, vertSource, fragSource, attrMap, description, fragDescription) {
        this.description = description;
        this.outputsToPick = description.includes("Overrides") || description.includes("Pick");
        this._fragDescription = fragDescription;
        this.vertSource = vertSource;
        this.fragSource = fragSource;
        this._attrMap = attrMap;
        const glProgram = gl.createProgram();
        this._glProgram = (null === glProgram) ? undefined : glProgram;
    }
    get isDisposed() { return this._glProgram === undefined; }
    [Symbol.dispose]() {
        if (!this.isDisposed) {
            assert(!this._inUse);
            System.instance.context.deleteProgram(expectDefined(this._glProgram));
            this._glProgram = undefined;
            this._status = 2 /* CompileStatus.Uncompiled */;
        }
    }
    /** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [Symbol.dispose] instead. */
    dispose() {
        this[Symbol.dispose]();
    }
    get glProgram() { return this._glProgram; }
    get isUncompiled() { return 2 /* CompileStatus.Uncompiled */ === this._status; }
    get isCompiled() { return 0 /* CompileStatus.Success */ === this._status; }
    compileShader(type) {
        const gl = System.instance.context;
        const shader = gl.createShader(type);
        if (null === shader)
            return undefined;
        const src = GL.ShaderType.Vertex === type ? this.vertSource : this.fragSource;
        gl.shaderSource(shader, src);
        gl.compileShader(shader);
        const succeeded = gl.getShaderParameter(shader, GL.ShaderParameter.CompileStatus);
        if (!succeeded) {
            const compileLog = `${GL.ShaderType.Vertex === type ? "Vertex" : "Fragment"} shader failed to compile. Errors: ${gl.getShaderInfoLog(shader)} Program description: ${this.description}`;
            throw new Error(compileLog);
        }
        if (System.instance.options.debugShaders) {
            const isVS = GL.ShaderType.Vertex === type;
            const desc = isVS ? this.description : this._fragDescription;
            this.saveShaderCode(isVS, desc, src, shader);
        }
        return shader;
    }
    linkProgram(vert, frag) {
        assert(undefined !== this.glProgram);
        if (undefined === this._glProgram || null === this._glProgram) // because WebGL APIs used Thing|null, not Thing|undefined...
            return false;
        const gl = System.instance.context;
        gl.attachShader(this._glProgram, vert);
        gl.attachShader(this._glProgram, frag);
        // bind attribute locations before final linking
        if (this._attrMap !== undefined) {
            for (const [key, attr] of this._attrMap) {
                gl.bindAttribLocation(this._glProgram, attr.location, key);
            }
        }
        gl.linkProgram(this._glProgram);
        const linkLog = gl.getProgramInfoLog(this._glProgram);
        gl.validateProgram(this._glProgram);
        const succeeded = gl.getProgramParameter(this._glProgram, GL.ProgramParameter.LinkStatus);
        if (!succeeded) {
            const validateLog = gl.getProgramInfoLog(this._glProgram);
            const msg = `Shader program failed to link. Link errors: ${linkLog} Validation errors: ${validateLog} Program description: ${this.description}`;
            throw new Error(msg);
        }
        // Debug output
        const debugUniforms = false;
        if (debugUniforms) {
            const dbgLog = (x) => console.log(x); // eslint-disable-line no-console
            const outSrc = false; // true for source out, false for just varying info
            if (this._fragDescription) {
                let tStr = "";
                if (!outSrc) {
                    tStr = `${this._fragDescription}`;
                }
                const numUniforms = gl.getProgramParameter(this._glProgram, GL.ProgramParameter.ActiveUniforms);
                const tStr2 = `Active Uniforms: ${numUniforms}`;
                if (tStr2) {
                    if (outSrc) {
                        dbgLog("//===============================================================================================================");
                        dbgLog(this.vertSource);
                        dbgLog("//========= Num Active Uniforms =========");
                        dbgLog(`${tStr2}\n`);
                        dbgLog(this.fragSource);
                    }
                    else {
                        dbgLog(`\n${tStr2}  ${tStr}\n`);
                    }
                }
            }
        }
        return true;
    }
    compile(forUse = false) {
        if (System.instance.options.debugShaders && forUse && this._status === 0 /* CompileStatus.Success */)
            this.setDebugShaderUsage();
        switch (this._status) {
            case 1 /* CompileStatus.Failure */: return 1 /* CompileStatus.Failure */;
            case 0 /* CompileStatus.Success */: return 0 /* CompileStatus.Success */;
            default: {
                if (this.isDisposed) {
                    this._status = 1 /* CompileStatus.Failure */;
                    return 1 /* CompileStatus.Failure */;
                }
                break;
            }
        }
        this._status = 1 /* CompileStatus.Failure */;
        const vert = this.compileShader(GL.ShaderType.Vertex);
        const frag = this.compileShader(GL.ShaderType.Fragment);
        if (undefined !== vert && undefined !== frag)
            if (this.linkProgram(vert, frag) && this.compileUniforms(this._programUniforms) && this.compileUniforms(this._graphicUniforms))
                this._status = 0 /* CompileStatus.Success */;
        if (System.instance.options.debugShaders && forUse && this._status === 0 /* CompileStatus.Success */)
            this.setDebugShaderUsage();
        if (true !== System.instance.options.preserveShaderSourceCode)
            this.vertSource = this.fragSource = "";
        return this._status;
    }
    use(params) {
        if (this.compile(true) !== 0 /* CompileStatus.Success */)
            return false;
        assert(undefined !== this._glProgram);
        if (null === this._glProgram || undefined === this._glProgram)
            return false;
        assert(!this._inUse);
        this._inUse = true;
        params.context.useProgram(this._glProgram);
        for (const uniform of this._programUniforms)
            uniform.bind(params);
        return true;
    }
    endUse() {
        this._inUse = false;
        System.instance.context.useProgram(null);
    }
    draw(params) {
        assert(this._inUse);
        for (const uniform of this._graphicUniforms)
            uniform.bind(params);
        params.geometry.draw();
    }
    addProgramUniform(name, binding) {
        assert(this.isUncompiled);
        this._programUniforms.push(new ProgramUniform(name, binding));
    }
    addGraphicUniform(name, binding) {
        assert(this.isUncompiled);
        this._graphicUniforms.push(new GraphicUniform(name, binding));
    }
    compileUniforms(uniforms) {
        for (const uniform of uniforms) {
            if (!uniform.compile(this))
                return false;
        }
        return true;
    }
    setDebugShaderUsage() {
        if (!System.instance.options.debugShaders)
            return;
        const shaderFiles = System.instance.debugShaderFiles;
        if (this._vertGNdx >= 0)
            shaderFiles[this._vertGNdx].isUsed = true;
        if (this._fragGNdx >= 0)
            shaderFiles[this._fragGNdx].isUsed = true;
        if (this._vertHNdx >= 0)
            shaderFiles[this._vertHNdx].isUsed = true;
        if (this._fragHNdx >= 0)
            shaderFiles[this._fragHNdx].isUsed = true;
    }
    saveShaderCode(isVS, desc, src, shader) {
        // save glsl and hlsl (from Angle and fixed up) in DebugShaderFile
        if (!System.instance.options.debugShaders)
            return;
        const shaderFiles = System.instance.debugShaderFiles;
        let sname;
        if (desc) {
            sname = desc.split(isVS ? "//!V! " : "//!F! ").join("");
            sname = sname.split(": ").join("-");
            sname = sname.split("; ").join("-");
        }
        else {
            // need to investigate shaders with no comments to derive names, for now come up with unique name
            sname = `noname-${shaderFiles.length}`;
        }
        sname += isVS ? "_VS" : "_FS";
        const fname = `${sname}.glsl`;
        let dsfNdx = shaderFiles.push(new DebugShaderFile(fname, src, isVS, true, false));
        if (isVS)
            this._vertGNdx = dsfNdx - 1;
        else
            this._fragGNdx = dsfNdx - 1;
        const ext2 = System.instance.context.getExtension("WEBGL_debug_shaders");
        if (!ext2)
            return;
        const srcH = ext2.getTranslatedShaderSource(shader);
        if (!srcH)
            return;
        // TODO: implement WebGL2 specific inputs for gl_VertexID and gl_InstanceID if ever used
        // parse and edit srcH to make it compilable
        const fnameH = `${sname}.hlsl`;
        let numTargets = 0; // for gl_Color cases
        let haveGLpos = false;
        let haveGLpntsz = false;
        let haveGLDepth = false;
        let haveGLFrontFacing = false;
        let haveGLPointCoord = false;
        let haveGLFragCoord = false;
        let haveGLFragColorOnly = false; // for only 1 output
        const haveGLFragColor = [false, false, false, false, false, false, false, false];
        const attrs = new Array();
        const varyings = new Array();
        const lines = srcH.split("\n");
        let toss = true;
        for (let ndx = 0; ndx < lines.length;) {
            let line = lines[ndx];
            if (line.indexOf("// INITIAL HLSL END") >= 0)
                toss = true;
            if (toss)
                lines.splice(ndx, 1);
            if (line.indexOf("// INITIAL HLSL BEGIN") >= 0) {
                toss = false;
            }
            else if (!toss) { // look for lines that need editing
                if (line.indexOf("Varyings") >= 0) { // save off varyings in either case
                    while (ndx + 1 < lines.length && lines[ndx + 1].indexOf("static") >= 0) {
                        ++ndx;
                        line = lines[ndx].substring(6).trimStart();
                        varyings.push(line.substring(0, line.indexOf("=")));
                    }
                }
                if (isVS) {
                    if (line.indexOf("Attributes") >= 0) { // save off attributes
                        while (ndx + 1 < lines.length && lines[ndx + 1].indexOf("static") >= 0) {
                            ++ndx;
                            line = lines[ndx].substring(6).trimStart();
                            attrs.push(line.substring(0, line.indexOf("=")));
                        }
                    }
                    else if (line.indexOf("static float4 gl_Position") >= 0) {
                        haveGLpos = true;
                    }
                    else if (line.indexOf("static float gl_PointSize") >= 0) {
                        haveGLpntsz = true;
                    }
                    else if (line.indexOf("@@ VERTEX ATTRIBUTES @@") >= 0) {
                        lines[ndx] = "// @@ VERTEX ATTRIBUTES @@";
                    }
                    else if (line.indexOf("@@ MAIN PROLOGUE @@") >= 0) {
                        lines[ndx] = "// @@ MAIN PROLOGUE @@\ngetInput(input);";
                    }
                    else if (line.indexOf("@@ VERTEX OUTPUT @@") >= 0) {
                        // have to create a VS_OUTPUT struct and a generateOutput function from varyings
                        lines[ndx] = "// @@ VERTEX OUTPUT @@\nstruct VS_INPUT\n  {";
                        let aNdx = 0;
                        for (const tstr of attrs) {
                            ++ndx;
                            lines.splice(ndx, 0, `  ${tstr}: TEXCOORD${aNdx};`);
                            ++aNdx;
                        }
                        ++ndx;
                        lines.splice(ndx, 0, "  };\nvoid getInput(VS_INPUT input) {");
                        for (const tstr of attrs) {
                            let t = tstr.indexOf("_a");
                            let vName = tstr.substring(t);
                            t = vName.indexOf(" ");
                            vName = vName.substring(0, t);
                            ++ndx;
                            lines.splice(ndx, 0, `  ${vName} = input.${vName};`);
                        }
                        ++ndx;
                        lines.splice(ndx, 0, "}\nstruct VS_OUTPUT\n  {");
                        if (haveGLpos) {
                            ++ndx;
                            lines.splice(ndx, 0, "  float4 _v_position : SV_Position;");
                        }
                        if (haveGLpntsz) {
                            ++ndx;
                            lines.splice(ndx, 0, "  float gl_PointSize : PointSize;");
                        }
                        let vNdx = 0;
                        for (const tstr of varyings) {
                            ++ndx;
                            lines.splice(ndx, 0, `  ${tstr}: TEXCOORD${vNdx};`);
                            ++vNdx;
                        }
                        ++ndx;
                        lines.splice(ndx, 0, "  };\nVS_OUTPUT generateOutput(VS_INPUT input) {\n  VS_OUTPUT output;");
                        if (haveGLpos) {
                            ++ndx;
                            lines.splice(ndx, 0, "  output._v_position = gl_Position;");
                        }
                        if (haveGLpntsz) {
                            ++ndx;
                            lines.splice(ndx, 0, "  output.gl_PointSize = gl_PointSize;");
                        }
                        for (const tstr of varyings) {
                            let t = tstr.indexOf("_v");
                            let vName = tstr.substring(t);
                            t = vName.indexOf(" ");
                            vName = vName.substring(0, t);
                            ++ndx;
                            lines.splice(ndx, 0, `  output.${vName} = ${vName};`);
                        }
                        ++ndx;
                        lines.splice(ndx, 0, "  return output;\n}");
                    }
                }
                else { // fragment shader
                    let tNdx = 0;
                    if (line.indexOf("static float4 gl_Color[") >= 0) {
                        //
                    }
                    else if (line.indexOf("gl_Color[0] =") >= 0) {
                        if (numTargets < 1)
                            numTargets = 1;
                    }
                    else if (line.indexOf("gl_Color[1] =") >= 0) {
                        if (numTargets < 2)
                            numTargets = 2;
                    }
                    else if (line.indexOf("gl_Color[2] =") >= 0) {
                        if (numTargets < 3)
                            numTargets = 3;
                    }
                    else if (line.indexOf("gl_Color[3] =") >= 0) {
                        numTargets = 4;
                    }
                    else if (line.indexOf("gl_Depth") >= 0) {
                        haveGLDepth = true;
                    }
                    else if (line.indexOf("gl_FrontFacing") >= 0) {
                        haveGLFrontFacing = true;
                    }
                    else if (line.indexOf("gl_PointCoord") >= 0) {
                        haveGLPointCoord = true;
                    }
                    else if (line.indexOf("gl_FragCoord") >= 0) {
                        haveGLFragCoord = true;
                    }
                    else if ((tNdx = line.indexOf("out_FragColor")) >= 0) {
                        const c = line.substring(tNdx + 13, tNdx + 13 + 1);
                        if (c === " " || c === "=")
                            haveGLFragColorOnly = true;
                        else {
                            tNdx = +c;
                            haveGLFragColor[tNdx] = true;
                        }
                    }
                    else if (line.indexOf("@@ PIXEL OUTPUT @@") >= 0) {
                        // have to create a VS_OUTPUT struct, a getInputs function (both from varyings),
                        // a PS_OUTPUT struct, and a generateOutput function (both based on numTargets or haveGLFragColor)
                        lines[ndx] = "// @@ PIXEL OUTPUT @@\nstruct VS_OUTPUT\n  {";
                        if (haveGLFragCoord) {
                            ++ndx;
                            lines.splice(ndx, 0, "  float4 gl_FragCoord : SV_POSITION;");
                        }
                        let vNdx = 0;
                        for (const tstr of varyings) {
                            ++ndx;
                            lines.splice(ndx, 0, `  ${tstr}: TEXCOORD${vNdx};`);
                            ++vNdx;
                        }
                        if (haveGLFrontFacing) {
                            ++ndx;
                            lines.splice(ndx, 0, "  bool gl_FrontFacing : SV_IsFrontFace;");
                        }
                        if (haveGLPointCoord) {
                            ++ndx;
                            lines.splice(ndx, 0, "  float2 gl_PointCoord : PointCoord;");
                        }
                        ++ndx;
                        lines.splice(ndx, 0, "  };\nvoid getInputs(VS_OUTPUT input) {");
                        if (haveGLFragCoord) {
                            ++ndx;
                            lines.splice(ndx, 0, "  gl_FragCoord = input.gl_FragCoord;");
                        }
                        for (const tstr of varyings) {
                            let t = tstr.indexOf("_v");
                            let vName = tstr.substring(t);
                            t = vName.indexOf(" ");
                            vName = vName.substring(0, t);
                            ++ndx;
                            lines.splice(ndx, 0, `  ${vName} = input.${vName};`);
                        }
                        if (haveGLFrontFacing) {
                            ++ndx;
                            lines.splice(ndx, 0, "  gl_FrontFacing = input.gl_FrontFacing;");
                        }
                        if (haveGLPointCoord) {
                            ++ndx;
                            lines.splice(ndx, 0, "  gl_PointCoord = input.gl_PointCoord;");
                        }
                        ++ndx;
                        lines.splice(ndx, 0, "}\nstruct PS_OUTPUT\n  {");
                        let cNdx = 0;
                        while (cNdx < numTargets) {
                            ++ndx;
                            lines.splice(ndx, 0, `  float4 col${cNdx} : SV_TARGET${cNdx};`);
                            ++cNdx;
                        }
                        if (haveGLFragColorOnly) {
                            ++ndx;
                            lines.splice(ndx, 0, "  float4 out_FragColor : SV_TARGET;");
                        }
                        else {
                            for (cNdx = 0; cNdx < haveGLFragColor.length; ++cNdx) {
                                if (haveGLFragColor[cNdx]) {
                                    ++ndx;
                                    lines.splice(ndx, 0, `  float4 out_FragColor${cNdx} : SV_TARGET${cNdx};`);
                                }
                            }
                        }
                        if (haveGLDepth) {
                            ++ndx;
                            lines.splice(ndx, 0, "  float gl_Depth : SV_Depth;");
                        }
                        ++ndx;
                        lines.splice(ndx, 0, "  };\nPS_OUTPUT generateOutput () {\n  PS_OUTPUT output;");
                        cNdx = 0;
                        while (cNdx < numTargets) {
                            ++ndx;
                            lines.splice(ndx, 0, `  output.col${cNdx} = gl_Color[${cNdx}];`);
                            ++cNdx;
                        }
                        if (haveGLFragColorOnly) {
                            ++ndx;
                            lines.splice(ndx, 0, "  output.out_FragColor = out_FragColor;");
                        }
                        else {
                            for (cNdx = 0; cNdx < haveGLFragColor.length; ++cNdx) {
                                if (haveGLFragColor[cNdx]) {
                                    ++ndx;
                                    lines.splice(ndx, 0, `  output.out_FragColor${cNdx} = out_FragColor${cNdx};`);
                                }
                            }
                        }
                        if (haveGLDepth) {
                            ++ndx;
                            lines.splice(ndx, 0, "  output.gl_Depth = gl_Depth;");
                        }
                        ++ndx;
                        lines.splice(ndx, 0, "  return output;\n}");
                    }
                    else if (line.indexOf("PS_OUTPUT main") >= 0) {
                        lines[ndx] = `// ${line}\nPS_OUTPUT main(VS_OUTPUT input){`;
                    }
                    else if (line.indexOf("@@ MAIN PROLOGUE @@") >= 0) {
                        lines[ndx] = `// ${line}\ngetInputs(input);`;
                    }
                }
                ++ndx;
            }
        }
        const srcH2 = lines.join("\n");
        dsfNdx = shaderFiles.push(new DebugShaderFile(fnameH, srcH2, isVS, false, false));
        if (isVS)
            this._vertHNdx = dsfNdx - 1;
        else
            this._fragHNdx = dsfNdx - 1;
    }
}
/** Context in which ShaderPrograms are executed. Avoids switching shaders unnecessarily.
 * Ensures shader programs are compiled before use and un-bound when scope is disposed.
 * Instances of this class must *only* be declared with the `using` keyword!
 * @internal
 */
export class ShaderProgramExecutor {
    _program;
    static _params;
    constructor(target, pass, program) {
        this.params.init(target, pass);
        this.changeProgram(program);
    }
    static freeParams() {
        this._params = undefined;
    }
    _isDisposed = false;
    get isDisposed() { return this._isDisposed; }
    /** Clears the current program to be executed. This does not free WebGL resources, since those are owned by Techniques. */
    [Symbol.dispose]() {
        this.changeProgram(undefined);
        ShaderProgramExecutor.freeParams();
        this._isDisposed = true;
    }
    /** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [Symbol.dispose] instead. */
    dispose() {
        this[Symbol.dispose]();
    }
    setProgram(program) { return this.changeProgram(program); }
    get isValid() { return undefined !== this._program; }
    get target() { return this.params.target; }
    get renderPass() { return this.params.renderPass; }
    get params() {
        if (undefined === ShaderProgramExecutor._params)
            ShaderProgramExecutor._params = new ShaderProgramParams();
        return ShaderProgramExecutor._params;
    }
    draw(params) {
        assert(this.isValid);
        if (undefined !== this._program) {
            this._program.draw(params);
        }
    }
    drawInterrupt(params) {
        assert(params.target === this.params.target);
        const tech = params.target.techniques.getTechnique(params.geometry.techniqueId);
        const program = tech.getShader(TechniqueFlags.defaults);
        if (this.setProgram(program)) {
            this.draw(params);
        }
    }
    pushBranch(branch) { this.target.pushBranch(branch); }
    popBranch() { this.target.popBranch(); }
    pushBatch(batch) { this.target.pushBatch(batch); }
    popBatch() { this.target.popBatch(); }
    changeProgram(program) {
        if (this._program === program) {
            return true;
        }
        else if (undefined !== this._program) {
            this._program.endUse();
        }
        this._program = program;
        if (undefined !== program && !program.use(this.params)) {
            this._program = undefined;
            return false;
        }
        return true;
    }
}
//# sourceMappingURL=ShaderProgram.js.map