import { Shader } from '../webgl/p5.Shader.js';
import { s as setWebGLUniformValue, e as setWebGLTextureParams, g as getWebGLUniformMetadata, h as getWebGLShaderAttributes, j as populateGLSLHooks, T as Texture } from '../rendering-C5SM-3b6.js';
import { I as Image } from '../p5.Renderer-N-APumjv.js';
import { C as CLAMP, ab as WEBGL2, n as CORNER, B as BLEND, ac as BLUR, ad as WEBGL } from '../constants-DwbuOBz3.js';
import { filterParamDefaults } from './const.js';
import { w as webgl2CompatibilityShader, f as filterBaseVert, a as filterBaseFrag } from '../webgl2Compatibility-DA7DLMuq.js';
import { g as glslBackend, r as randomGLSL, a as randomVertGLSL } from '../strands_glslBackend-DMhOnoGl.js';
import { getShaderHookTypes } from '../webgl/shaderHookUtils.js';
import { makeFilterShader } from '../core/filterShaders.js';
import '../creating_reading-Be7_6X4p.js';
import 'colorjs.io/fn';
import '../color/color_spaces/hsb.js';
import '../strands/ir_types.js';
import '../dom/p5.Element.js';
import '../dom/p5.File.js';
import '../io/p5.XML.js';
import '../dom/p5.MediaElement.js';
import '../shape/2d_primitives.js';
import '../core/helpers.js';
import '../shape/attributes.js';
import '../shape/curves.js';
import '../shape/vertex.js';
import '../color/setting.js';
import 'omggif';
import '../io/csv.js';
import '../io/utilities.js';
import './pixels.js';
import './filters.js';
import '../core/transform.js';
import '../webgl/GeometryBuilder.js';
import '../math/p5.Matrix.js';
import '../math/Matrices/Matrix.js';
import '../math/p5.Vector.js';
import '../math/Matrices/MatrixInterface.js';
import '../webgl/p5.Geometry.js';
import '../webgl/p5.DataArray.js';
import '../webgl/p5.Quat.js';
import '../webgl/ShapeBuilder.js';
import 'libtess';
import '../webgl/p5.RenderBuffer.js';
import '../webgl/GeometryBufferCache.js';
import '../shape/custom_shapes.js';
import '../type/textCore.js';
import '../core/States.js';
import '../webgl/enums.js';
import '../math/trigonometry.js';
import '../strands/ir_dag.js';
import '../strands/strands_FES.js';
import '../ir_builders-CMXkjMoV.js';
import '../strands/ir_cfg.js';
import '../strands/strands_builtins.js';

class FilterRenderer2D {
  /**
   * Creates a new FilterRenderer2D instance.
   * @param {p5} parentRenderer - The p5.js instance.
   */
  constructor(parentRenderer) {
    this.parentRenderer = parentRenderer;
    // Create a canvas for applying WebGL-based filters
    this.canvas = document.createElement('canvas');
    this.canvas.width = parentRenderer.width;
    this.canvas.height = parentRenderer.height;

    // Initialize the WebGL context
    let webglVersion = WEBGL2;
    this.gl = this.canvas.getContext('webgl2');
    if (!this.gl) {
      webglVersion = WEBGL;
      this.gl = this.canvas.getContext('webgl');
    }
    if (!this.gl) {
      console.error('WebGL not supported, cannot apply filter.');
      return;
    }
    this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

    this.textures = new Map();

    // Minimal renderer object required by p5.Shader and p5.Texture
    this._renderer = {
      GL: this.gl,
      registerEnabled: new Set(),
      _curShader: null,
      _emptyTexture: null,
      webglVersion,
      states: {
        textureWrapX: CLAMP,
        textureWrapY: CLAMP,
      },
      _arraysEqual: (a, b) => JSON.stringify(a) === JSON.stringify(b),
      _getEmptyTexture: () => {
        if (!this._emptyTexture) {
          const im = new Image(1, 1);
          im.set(0, 0, 255);
          this._emptyTexture = new Texture(this._renderer, im);
        }
        return this._emptyTexture;
      },
      _initShader: (shader) => {
        const gl = this.gl;

        const vertShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertShader, shader.vertSrc());
        gl.compileShader(vertShader);
        if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) {
          throw new Error(`Yikes! An error occurred compiling the vertex shader: ${
            gl.getShaderInfoLog(vertShader)
          }`);
        }

        const fragShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragShader, shader.fragSrc());
        gl.compileShader(fragShader);
        if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) {
          throw new Error(`Darn! An error occurred compiling the fragment shader: ${
            gl.getShaderInfoLog(fragShader)
          }`);
        }

        const program = gl.createProgram();
        gl.attachShader(program, vertShader);
        gl.attachShader(program, fragShader);
        gl.linkProgram(program);

        if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
          throw new Error(
            `Snap! Error linking shader program: ${gl.getProgramInfoLog(program)}`
          );
        }

        shader._glProgram = program;
        shader._vertShader = vertShader;
        shader._fragShader = fragShader;
      },
      getTexture: (input) => {
        let src = input;
        if (src instanceof Framebuffer) {
          src = src.color;
        }

        const texture = this.textures.get(src);
        if (texture) {
          return texture;
        }

        const tex = new Texture(this._renderer, src);
        this.textures.set(src, tex);
        return tex;
      },
      populateHooks: (shader, src, shaderType) => {
        return populateGLSLHooks(shader, src, shaderType);
      },
      _getShaderAttributes: (shader) => {
        return getWebGLShaderAttributes(shader, this.gl);
      },
      getUniformMetadata: (shader) => {
        return getWebGLUniformMetadata(shader, this.gl);
      },
      _finalizeShader: () => {},
      _useShader: (shader) => {
        this.gl.useProgram(shader._glProgram);
      },
      bindTexture: (tex) => {
        // bind texture using gl context + glTarget and
        // generated gl texture object
        this.gl.bindTexture(this.gl.TEXTURE_2D, tex.getTexture().texture);
      },
      unbindTexture: () => {
        // unbind per above, disable texturing on glTarget
        this.gl.bindTexture(this.gl.TEXTURE_2D, null);
      },
      _unbindFramebufferTexture: (uniform) => {
        // Make sure an empty texture is bound to the slot so that we don't
        // accidentally leave a framebuffer bound, causing a feedback loop
        // when something else tries to write to it
        const gl = this.gl;
        const empty = this._getEmptyTexture();
        gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex);
        empty.bindTexture();
        gl.uniform1i(uniform.location, uniform.samplerIndex);
      },
      createTexture: ({ width, height, format, dataType }) => {
        const gl = this.gl;
        const tex = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, tex);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0,
                           gl.RGBA, gl.UNSIGNED_BYTE, null);
        // TODO use format and data type
        return { texture: tex, glFormat: gl.RGBA, glDataType: gl.UNSIGNED_BYTE };
      },
      uploadTextureFromSource: ({ texture, glFormat, glDataType }, source) => {
        const gl = this.gl;
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(gl.TEXTURE_2D, 0, glFormat, glFormat, glDataType, source);
      },
      uploadTextureFromData: ({ texture, glFormat, glDataType }, data, width, height) => {
        const gl = this.gl;
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(
          gl.TEXTURE_2D,
          0,
          glFormat,
          width,
          height,
          0,
          glFormat,
          glDataType,
          data
        );
      },
      setTextureParams: (texture) => {
        return setWebGLTextureParams(texture, this.gl, this._renderer.webglVersion);
      },
      updateUniformValue: (shader, uniform, data) => {
        return setWebGLUniformValue(
          shader,
          uniform,
          data,
          (tex) => this._renderer.getTexture(tex),
          this.gl
        );
      },
      _enableAttrib: (_shader, attr, size, type, normalized, stride, offset) => {
        const loc = attr.location;
        const gl = this.gl;
        // Enable register even if it is disabled
        if (!this._renderer.registerEnabled.has(loc)) {
          gl.enableVertexAttribArray(loc);
          // Record register availability
          this._renderer.registerEnabled.add(loc);
        }
        gl.vertexAttribPointer(
          loc,
          size,
          type || gl.FLOAT,
          normalized || false,
          stride || 0,
          offset || 0
        );
      },
      _disableRemainingAttributes: (shader) => {
        for (const location of this._renderer.registerEnabled.values()) {
          if (
            !Object.keys(shader.attributes).some(
              key => shader.attributes[key].location === location
            )
          ) {
            this.gl.disableVertexAttribArray(location);
            this._renderer.registerEnabled.delete(location);
          }
        }
      },
      _updateTexture: (uniform, tex) => {
        const gl = this.gl;
        gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex);
        tex.bindTexture();
        tex.update();
        gl.uniform1i(uniform.location, uniform.samplerIndex);
      },
      baseFilterShader: () => this.baseFilterShader(),
      strandsBackend: glslBackend,
      getShaderHookTypes: (shader, hookName) => getShaderHookTypes(shader, hookName),
      uniformNameFromHookKey: (key) => key.slice(key.indexOf(' ') + 1),
    };

    this._baseFilterShader = undefined;

    // Store initialized shaders for each operation
    this.filterShaders = {};

    // These will be set by setOperation
    this.operation = null;
    this.filterParameter = 1;
    this.customShader = null;
    this._shader = null;

    // Create buffers once
    this.vertexBuffer = this.gl.createBuffer();
    this.texcoordBuffer = this.gl.createBuffer();

    // Set up the vertices and texture coordinates for a full-screen quad
    this.vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
    this.texcoords = new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]);

    // Upload vertex data once
    this._bindBufferData(
      this.vertexBuffer,
      this.gl.ARRAY_BUFFER,
      this.vertices
    );

    // Upload texcoord data once
    this._bindBufferData(
      this.texcoordBuffer,
      this.gl.ARRAY_BUFFER,
      this.texcoords
    );
  }

  _webGL2CompatibilityPrefix(shaderType, floatPrecision) {
    let code = '';
    if (this._renderer.webglVersion === WEBGL2) {
      code += '#version 300 es\n#define WEBGL2\n';
    }
    if (shaderType === 'vert') {
      code += '#define VERTEX_SHADER\n';
    } else if (shaderType === 'frag') {
      code += '#define FRAGMENT_SHADER\n';
    }
    if (floatPrecision) {
      code += `precision ${floatPrecision} float;\n`;
    }
    return code;
  }

  baseFilterShader() {
    if (!this._baseFilterShader) {
      this._baseFilterShader = new Shader(
        this._renderer,
        this._webGL2CompatibilityPrefix('vert', 'highp') +
          webgl2CompatibilityShader +
          filterBaseVert,
        this._webGL2CompatibilityPrefix('frag', 'highp') +
          webgl2CompatibilityShader +
          filterBaseFrag,
        {
          vertex: {},
          fragment: {
            'vec4 getColor': `(FilterInputs inputs, in sampler2D canvasContent) {
              return getTexture(canvasContent, inputs.texCoord);
            }`
          },
          hookAliases: {
            'getColor': ['filterColor'],
          },
        }
      );
    }
    return this._baseFilterShader;
  }


  getRandomFragmentShaderSnippet() {
    return randomGLSL;
  }

  getRandomVertexShaderSnippet() {
    return randomVertGLSL;
  }

  /**
   * Set the current filter operation and parameter. If a customShader is provided,
   * that overrides the operation-based shader.
   * @param {String} operation - The filter operation type (e.g., constants.BLUR).
   * @param {Number} filterParameter - The strength of the filter.
   * @param {p5.Shader} customShader - Optional custom shader.
   */
  setOperation(operation, filterParameter, customShader = null) {
    this.operation = operation;
    this.filterParameter = filterParameter;

    let useDefaultParam = operation in filterParamDefaults &&
      filterParameter === undefined;
    if (useDefaultParam) {
      this.filterParameter = filterParamDefaults[operation];
    }

    this.customShader = customShader;
    this._initializeShader();
  }

  /**
   * Initializes or retrieves the shader program for the current operation.
   * If a customShader is provided, that is used.
   * Otherwise, returns a cached shader if available, or creates a new one, caches it, and sets it as current.
   */
  _initializeShader() {
    if (this.customShader) {
      this._shader = this.customShader;
      return;
    }

    if (!this.operation) {
      console.error('No operation set for FilterRenderer2D, cannot initialize shader.');
      return;
    }

    // If we already have a compiled shader for this operation, reuse it
    if (this.filterShaders[this.operation]) {
      this._shader = this.filterShaders[this.operation];
      return;
    }

    // Use the shared makeFilterShader function from filterShaders.js
    const newShader = makeFilterShader(this._renderer, this.operation, this.parentRenderer._pInst);
    this.filterShaders[this.operation] = newShader;
    this._shader = newShader;
  }

  /**
   * Binds a buffer to the drawing context
   * when passed more than two arguments it also updates or initializes
   * the data associated with the buffer
   */
  _bindBufferData(buffer, target, values) {
    const gl = this.gl;
    gl.bindBuffer(target, buffer);
    gl.bufferData(target, values, gl.STATIC_DRAW);
  }

  get canvasTexture() {
    if (!this._canvasTexture) {
      this._canvasTexture = new Texture(this._renderer, this.parentRenderer);
    }
    return this._canvasTexture;
  }

  /**
   * Prepares and runs the full-screen quad draw call.
   */
  _renderPass() {
    const gl = this.gl;
    this._shader.bindShader('fill');
    const pixelDensity = this.parentRenderer.pixelDensity ?
      this.parentRenderer.pixelDensity() : 1;

    const texelSize = [
      1 / (this.parentRenderer.width * pixelDensity),
      1 / (this.parentRenderer.height * pixelDensity)
    ];

    const canvasTexture = this.canvasTexture;

    // Set uniforms for the shader
    this._shader.setUniform('tex0', canvasTexture);
    this._shader.setUniform('texelSize', texelSize);
    this._shader.setUniform('canvasSize', [this.parentRenderer.width, this.parentRenderer.height]);
    this._shader.setUniform('radius', Math.max(1, this.filterParameter));
    this._shader.setUniform('filterParameter', this.filterParameter);
    this._shader.setDefaultUniforms();

    this.parentRenderer.states.setValue('rectMode', CORNER);
    this.parentRenderer.states.setValue('imageMode', CORNER);
    this.parentRenderer.blendMode(BLEND);
    this.parentRenderer.resetMatrix();


    const identityMatrix = [1, 0, 0, 0,
                            0, 1, 0, 0,
                            0, 0, 1, 0,
                            0, 0, 0, 1];
    this._shader.setUniform('uModelViewMatrix', identityMatrix);
    this._shader.setUniform('uProjectionMatrix', identityMatrix);

    // Bind and enable vertex attributes
    gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
    this._shader.enableAttrib(this._shader.attributes.aPosition, 2);

    gl.bindBuffer(gl.ARRAY_BUFFER, this.texcoordBuffer);
    this._shader.enableAttrib(this._shader.attributes.aTexCoord, 2);

    this._shader.bindTextures();
    this._renderer._disableRemainingAttributes(this._shader);

    // Draw the quad
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    // Unbind the shader
    this._shader.unbindShader();
  }

  /**
   * Applies the current filter operation. If the filter requires multiple passes (e.g. blur),
   * it handles those internally. Make sure setOperation() has been called before applyFilter().
   */
  applyFilter() {
    if (!this._shader) {
      console.error('Cannot apply filter: shader not initialized.');
      return;
    }
    this.parentRenderer.push();
    this.parentRenderer.resetMatrix();
    // For blur, we typically do two passes: one horizontal, one vertical.
    if (this.operation === BLUR && !this.customShader) {
      // Horizontal pass
      this._shader.setUniform('direction', [1, 0]);
      this._renderPass();

      // Draw the result onto itself
      this.parentRenderer.clear();
      this.parentRenderer.drawingContext.drawImage(
        this.canvas,
        0, 0,
        this.parentRenderer.width, this.parentRenderer.height
      );

      // Vertical pass
      this._shader.setUniform('direction', [0, 1]);
      this._renderPass();

      this.parentRenderer.clear();
      this.parentRenderer.drawingContext.drawImage(
        this.canvas,
        0, 0,
        this.parentRenderer.width, this.parentRenderer.height
      );
    } else {
      // Single-pass filters

      this._renderPass();
      this.parentRenderer.clear();
      // con
      this.parentRenderer.blendMode(BLEND);


      this.parentRenderer.drawingContext.drawImage(
        this.canvas,
        0, 0,
        this.parentRenderer.width, this.parentRenderer.height
      );
    }
    this.parentRenderer.pop();
  }
}

export { FilterRenderer2D as default };
