/**
 * This module defines the p5.Shader class
 * @module 3D
 * @submodule Material
 * @for p5
 */

const TypedArray = Object.getPrototypeOf(Uint8Array);
class Shader {
  constructor(renderer, vertSrc, fragSrc, options = {}) {
    this._renderer = renderer;

    // Detect compute shader: first arg is STRING and second is undefined OR an options object
    if (
      typeof vertSrc === 'string' && (
        fragSrc === undefined || (typeof fragSrc === 'object' && !Array.isArray(fragSrc))
      )
    ) {
      // Compute shader
      this.shaderType = 'compute';
      this._computeSrc = vertSrc;
      this._vertSrc = null;
      this._fragSrc = null;
      // If fragSrc is an options object, use it
      if (typeof fragSrc === 'object') {
        options = fragSrc;
      }
    } else {
      // Render shader - shaderType will be set later during binding ('fill', 'stroke', etc.)
      this._vertSrc = vertSrc;
      this._fragSrc = fragSrc;
      this._computeSrc = null;
    }

    this._vertShader = -1;
    this._fragShader = -1;
    this._compiled = false;
    this._loadedAttributes = false;
    this.attributes = {};
    this._loadedUniforms = false;
    this.uniforms = {};
    this._bound = false;
    this.samplers = [];
    this.hooks = {
      // These should be passed in by `.modify()` instead of being manually
      // passed in.

      // Stores uniforms + default values.
      uniforms: options.uniforms || {},

      // Compute shader storage uniforms + default values
      storageUniforms: options.storageUniforms || {},

      // Maps user-facing inferred names to internal shader-facing names.
      shaderNameMap: options.shaderNameMap || {
        externalToInternal: {},
        internalToExternal: {}
      },

      // Tracks the next auto-generated internal shader-name suffix for chained modify() calls.
      shaderNameState: options.shaderNameState || {
        nextSuffix: 0
      },

      // Stores custom uniform + helper declarations as a string.
      declarations: options.declarations,

      // Stores an array of variable names + types passed between the vertex and fragment shader
      varyingVariables: options.varyingVariables || [],

      // Stores instanceID varying info for forwarding to the fragment shader
      instanceIDVarying: options.instanceIDVarying || null,

      // Stores helper functions to prepend to shaders.
      helpers: options.helpers || {},

      // Stores the hook implementations
      vertex: options.vertex || {},
      fragment: options.fragment || {},
      compute: options.compute || {},

      hookAliases: options.hookAliases || {},

      // Stores whether or not the hook implementation has been modified
      // from the default. This is supplied automatically by calling
      // yourShader.modify(...).
      modified: {
        vertex: (options.modified && options.modified.vertex) || {},
        fragment: (options.modified && options.modified.fragment) || {},
        compute: (options.modified && options.modified.compute) || {},
      }
    };
  }

  hookTypes(hookName) {
    return this._renderer.getShaderHookTypes(this, hookName);
  }

  shaderSrc(src, shaderType) {
    return this._renderer.populateHooks(this, src, shaderType);
  }

  /**
   * Shaders are written in <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders">GLSL</a>, but
   * there are different versions of GLSL that it might be written in.
   *
   * Calling this method on a `p5.Shader` will return the GLSL version it uses, either `100 es` or `300 es`.
   * WebGL 1 shaders will only use `100 es`, and WebGL 2 shaders may use either.
   *
   * @returns {String} The GLSL version used by the shader.
   */
  version() {
    const match = /#version (.+)$/m.exec(this.vertSrc());
    if (match) {
      return match[1];
    } else {
      return '100 es';
    }
  }

  vertSrc() {
    if (this.shaderType === 'compute') return null;
    return this.shaderSrc(this._vertSrc, 'vertex');
  }

  fragSrc() {
    if (this.shaderType === 'compute') return null;
    return this.shaderSrc(this._fragSrc, 'fragment');
  }

  computeSrc() {
    if (this.shaderType !== 'compute') return null;
    return this.shaderSrc(this._computeSrc, 'compute');
  }

  /**
   * Logs the hooks available in this shader, and their current implementation.
   *
   * Each shader may let you override bits of its behavior. Each bit is called
   * a *hook.* A hook is either for the *vertex* shader, if it affects the
   * position of vertices, or in the *fragment* shader, if it affects the pixel
   * color. This method logs those values to the console, letting you know what
   * you are able to use in a call to
   * <a href="#/p5.Shader/modify">`modify()`</a>.
   *
   * For example, this shader will produce the following output:
   *
   * ```js
   * myShader = baseMaterialShader().modify({
   *   declarations: 'uniform float time;',
   *   'vec3 getWorldPosition': `(vec3 pos) {
   *     pos.y += 20. * sin(time * 0.001 + pos.x * 0.05);
   *     return pos;
   *   }`
   * });
   * myShader.inspectHooks();
   * ```
   *
   * ```
   * ==== Vertex shader hooks: ====
   * void beforeVertex() {}
   * vec3 getLocalPosition(vec3 position) { return position; }
   * [MODIFIED] vec3 getWorldPosition(vec3 pos) {
   *       pos.y += 20. * sin(time * 0.001 + pos.x * 0.05);
   *       return pos;
   *     }
   * vec3 getLocalNormal(vec3 normal) { return normal; }
   * vec3 getWorldNormal(vec3 normal) { return normal; }
   * vec2 getUV(vec2 uv) { return uv; }
   * vec4 getVertexColor(vec4 color) { return color; }
   * void afterVertex() {}
   *
   * ==== Fragment shader hooks: ====
   * void beforeFragment() {}
   * Inputs getPixelInputs(Inputs inputs) { return inputs; }
   * vec4 combineColors(ColorComponents components) {
   *                 vec4 color = vec4(0.);
   *                 color.rgb += components.diffuse * components.baseColor;
   *                 color.rgb += components.ambient * components.ambientColor;
   *                 color.rgb += components.specular * components.specularColor;
   *                 color.rgb += components.emissive;
   *                 color.a = components.opacity;
   *                 return color;
   *               }
   * vec4 getFinalColor(vec4 color, vec2 texCoord) { return color; }
   * void afterFragment() {}
   * ```
   *
   * @beta
   */
  inspectHooks() {
    if (this.shaderType === 'compute') {
      console.log('==== Compute shader hooks: ====');
      for (const key in this.hooks.compute) {
        console.log(
          (this.hooks.modified.compute[key] ? '[MODIFIED] ' : '') +
            key +
            this.hooks.compute[key]
        );
      }
    } else {
      console.log('==== Vertex shader hooks: ====');
      for (const key in this.hooks.vertex) {
        console.log(
          (this.hooks.modified.vertex[key] ? '[MODIFIED] ' : '') +
            key +
            this.hooks.vertex[key]
        );
      }
      console.log('');
      console.log('==== Fragment shader hooks: ====');
      for (const key in this.hooks.fragment) {
        console.log(
          (this.hooks.modified.fragment[key] ? '[MODIFIED] ' : '') +
            key +
            this.hooks.fragment[key]
        );
      }
    }
    console.log('');
    console.log('==== Helper functions: ====');
    for (const key in this.hooks.helpers) {
      console.log(key + this.hooks.helpers[key]);
    }
  }

  /**
   * Returns a new shader, based on the original, but with custom snippets
   * of shader code replacing default behaviour.
   *
   * Each shader may let you override bits of its behavior. Each bit is called
   * a *hook.* For example, a hook can let you adjust positions of vertices, or
   * the color of a pixel. You can inspect the different hooks available by calling
   * <a href="#/p5.Shader/inspectHooks">`yourShader.inspectHooks()`</a>. You can
   * also read the reference for the default material, normal material, color, line, and point shaders to
   * see what hooks they have available.
   *
   * `modify()` can be passed a function as a parameter. Inside, you can override hooks
   * by calling them as functions. Each hook will take in a callback that takes in inputs
   * and is expected to return an output. For example, here is a function that changes the
   * material color to red:
   *
   * ```js example
   * let myShader;
   *
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = baseMaterialShader().modify(() => {
   *     getPixelInputs((inputs) => {
   *       inputs.color = [inputs.texCoord, 0, 1];
   *       return inputs;
   *     });
   *   });
   * }
   *
   * function draw() {
   *   background(255);
   *   noStroke();
   *   shader(myShader); // Apply the custom shader
   *   plane(width, height); // Draw a plane with the shader applied
   * }
   * ```
   *
   * In addition to calling hooks, you can create uniforms, which are special variables
   * used to pass data from p5.js into the shader. They can be created by calling `uniform` + the
   * type of the data, such as `uniformFloat` for a number or `uniformVector2` for a two-component vector.
   * They take in a function that returns the data for the variable. You can then reference these
   * variables in your hooks, and their values will update every time you apply
   * the shader with the result of your function.
   *
   * Move the mouse over this sketch to increase the moveCounter which will be passed to the shader as a uniform.
   *
   * ```js example
   * let myShader;
   * //count of frames in which mouse has been moved
   * let moveCounter = 0;
   *
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = baseMaterialShader().modify(() => {
   *     // Get the move counter from our sketch
   *     let count = uniformFloat(() => moveCounter);
   *
   *     getPixelInputs((inputs) => {
   *       inputs.color = [
   *         inputs.texCoord,
   *         sin(count/100) / 2 + 0.5,
   *         1,
   *       ];
   *       return inputs;
   *     });
   *   });
   * }
   *
   * function mouseDragged(){
   *   moveCounter += 1;
   * }
   *
   * function draw() {
   *   background(255);
   *   noStroke(255);
   *   shader(myShader); // Apply the custom shader
   *   plane(width, height); // Draw a plane with the shader applied
   * }
   * ```
   *
   * p5.strands functions are special, since they get turned into a shader instead of being
   * run like the rest of your code. They only have access to p5.js functions, and variables
   * you declare inside the `modify` callback. If you need access to local variables, you
   * can pass them into `modify` with an optional second parameter, `variables`. These will
   * then be passed into your function as an argument. If you are
   * using instance mode, you will need to pass your sketch object in this way.
   *
   * If you are also using a build system for your sketch, variable names may be changed as
   * part of minification. When creating a uniform, you can pass the name of the uniform in
   * as a first parameter to ensure it doesn't get changed.
   *
   * ```js example
   * new p5((sketch) => {
   *   let myShader;
   *
   *   sketch.setup = function() {
   *     sketch.createCanvas(200, 200, sketch.WEBGL);
   *     myShader = sketch.baseMaterialShader().modify(({ sketch }) => {
   *       let b = sketch.uniformFloat('b');
   *       sketch.getPixelInputs((inputs) => {
   *         inputs.color = [inputs.texCoord, b, 1];
   *         return inputs;
   *       });
   *     }, { sketch });
   *   }
   *
   *   sketch.draw = function() {
   *     sketch.background(255);
   *     sketch.noStroke();
   *     myShader.setUniform('b', 0.5);
   *     sketch.shader(myShader); // Apply the custom shader
   *     sketch.plane(sketch.width, sketch.height); // Draw a plane with the shader applied
   *   }
   * });
   * ```
   *
   * You can also write GLSL directly in `modify` if you need direct access. To do so,
   * `modify()` takes one parameter, `hooks`, an object with the hooks you want
   * to override. Each key of the `hooks` object is the name
   * of a hook, and the value is a string with the GLSL code for your hook.
   *
   * If you supply functions that aren't existing hooks, they will get added at the start of
   * the shader as helper functions so that you can use them in your hooks.
   *
   * To add new <a href="#/p5.Shader/setUniform">uniforms</a> to your shader, you can pass in a `uniforms` object containing
   * the type and name of the uniform as the key, and a default value or function returning
   * a default value as its value. These will be automatically set when the shader is set
   * with `shader(yourShader)`.
   *
   * ```js example
   * let myShader;
   *
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = baseMaterialShader().modify({
   *     uniforms: {
   *       'float time': () => millis() // Uniform for time
   *     },
   *     'Vertex getWorldInputs': `(Vertex inputs) {
   *       inputs.position.y +=
   *         20. * sin(time * 0.001 + inputs.position.x * 0.05);
   *       return inputs;
   *     }`
   *   });
   * }
   *
   * function draw() {
   *   background(255);
   *   shader(myShader); // Apply the custom shader
   *   lights();         // Enable lighting
   *   noStroke();       // Disable stroke
   *   fill('red');      // Set fill color to red
   *   sphere(50);       // Draw a sphere with the shader applied
   * }
   * ```
   *
   * You can also add a `declarations` key, where the value is a GLSL string declaring
   * custom uniform variables, globals, and functions shared
   * between hooks. To add declarations just in a vertex or fragment shader, add
   * `vertexDeclarations` and `fragmentDeclarations` keys.
   *
   * ```js example
   * let myShader;
   *
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = baseMaterialShader().modify({
   *     // Manually specifying a uniform
   *     declarations: 'uniform float time;',
   *     'Vertex getWorldInputs': `(Vertex inputs) {
   *       inputs.position.y +=
   *         20. * sin(time * 0.001 + inputs.position.x * 0.05);
   *       return inputs;
   *     }`
   *   });
   * }
   *
   * function draw() {
   *   background(255);
   *   shader(myShader);
   *   myShader.setUniform('time', millis());
   *   lights();
   *   noStroke();
   *   fill('red');
   *   sphere(50);
   * }
   * ```
   *
   * @beta
   * @param {Function} callback A function with p5.strands code to modify the shader.
   * @param {Object} [variables] An optional object with local variables p5.strands
   * should have access to.
   * @returns {p5.Shader}
   */
  /**
   * @param {Object} [hooks] The hooks in the shader to replace.
   * @returns {p5.Shader}
   */
  modify(hooks) {
    // p5._validateParameters('p5.Shader.modify', arguments);
    const newHooks = {
      vertex: {},
      fragment: {},
      compute: {},
      helpers: {}
    };
    for (const key in hooks) {
      if (key === 'declarations') continue;
      if (key === 'uniforms') continue;
      if (key === 'storageUniforms') continue;
      if (key === 'shaderNameMap') continue;
      if (key === 'shaderNameState') continue;
      if (key === 'varyingVariables') continue;
      if (key === 'instanceIDVarying') continue;
      if (key === 'vertexDeclarations') {
        newHooks.vertex.declarations =
          (newHooks.vertex.declarations || '') + '\n' + hooks[key];
      } else if (key === 'fragmentDeclarations') {
        newHooks.fragment.declarations =
          (newHooks.fragment.declarations || '') + '\n' + hooks[key];
      } else if (key === 'computeDeclarations') {
        newHooks.compute.declarations =
          (newHooks.compute.declarations || '') + '\n' + hooks[key];
      } else if (this.hooks.vertex[key]) {
        newHooks.vertex[key] = hooks[key];
      } else if (this.hooks.fragment[key]) {
        newHooks.fragment[key] = hooks[key];
      } else if (this.hooks.compute[key]) {
        newHooks.compute[key] = hooks[key];
      } else {
        newHooks.helpers[key] = hooks[key];
      }
    }
    const modifiedVertex = Object.assign({}, this.hooks.modified.vertex);
    const modifiedFragment = Object.assign({}, this.hooks.modified.fragment);
    const modifiedCompute = Object.assign({}, this.hooks.modified.compute);
    for (const key in newHooks.vertex || {}) {
      if (key === 'declarations') continue;
      modifiedVertex[key] = true;
    }
    for (const key in newHooks.fragment || {}) {
      if (key === 'declarations') continue;
      modifiedFragment[key] = true;
    }
    for (const key in newHooks.compute || {}) {
      if (key === 'declarations') continue;
      modifiedCompute[key] = true;
    }

    const args = [this._renderer];
    if (this.shaderType === 'compute') {
      args.push(this._computeSrc);
    } else {
      args.push(this._vertSrc, this._fragSrc);
    }
    args.push({
      declarations:
        (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''),
      uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}),
      storageUniforms: Object.assign({}, this.hooks.storageUniforms, hooks.storageUniforms || {}),
      // Preserve shader name remapping and suffix state across chained modify()
      // calls so later passes keep earlier internal names and keep incrementing.
      shaderNameMap: {
        externalToInternal: Object.assign(
          {},
          this.hooks.shaderNameMap?.externalToInternal ?? {},
          hooks.shaderNameMap?.externalToInternal ?? {}
        ),
        internalToExternal: Object.assign(
          {},
          this.hooks.shaderNameMap?.internalToExternal ?? {},
          hooks.shaderNameMap?.internalToExternal ?? {}
        )
      },
      shaderNameState: {
        nextSuffix:
          hooks.shaderNameState?.nextSuffix ??
          this.hooks.shaderNameState?.nextSuffix ??
          0
      },
      varyingVariables: (hooks.varyingVariables || []).concat(this.hooks.varyingVariables || []),
      instanceIDVarying: hooks.instanceIDVarying || this.hooks.instanceIDVarying || null,
      fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}),
      vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}),
      compute: Object.assign({}, this.hooks.compute, newHooks.compute || {}),
      helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}),
      hookAliases: Object.assign({}, this.hooks.hookAliases, newHooks.hookAliases || {}),
      modified: {
        vertex: modifiedVertex,
        fragment: modifiedFragment,
        compute: modifiedCompute,
      }
    });

    return new Shader(...args);
  }

  /**
   * Creates, compiles, and links the shader based on its
   * sources for the vertex and fragment shaders (provided
   * to the constructor). Populates known attributes and
   * uniforms from the shader.
   * @chainable
   * @private
   */
  init() {
    // If the shader is uninitialized or context was lost
    if (!this._initialized) {
      try {
        this._renderer._initShader(this); // Backend-specific shader init
      } catch (err) {
        throw new Error(
          `Whoops! Something went wrong initializing the shader:\n${err.message || err}`
        );
      }

      if (this.shaderType !== 'compute') {
        this._loadAttributes();
      }
      this._loadUniforms();
      this._renderer._finalizeShader(this);

      this._initialized = true;
    }

    return this;
  }

  /**
   * @private
   */
  setDefaultUniforms() {
    for (const key in this.hooks.uniforms) {
      const name = this._renderer.uniformNameFromHookKey(key);
      const initializer = this.hooks.uniforms[key];
      let value;
      if (initializer instanceof Function) {
        value = initializer();
      } else {
        value = initializer;
      }

      if (value !== undefined && value !== null) {
        this.setUniform(name, value);
      }
    }
    for (const name in this.hooks.storageUniforms) {
      const initializer = this.hooks.storageUniforms[name];
      const value = initializer instanceof Function ? initializer() : initializer;
      if (value !== undefined && value !== null) {
        this.setUniform(name, value);
      }
    }
  }

  /**
   * Copies the shader from one drawing context to another.
   *
   * Each `p5.Shader` object must be compiled by calling
   * <a href="#/p5/shader">shader()</a> before it can run. Compilation happens
   * in a drawing context which is usually the main canvas or an instance of
   * <a href="#/p5.Graphics">p5.Graphics</a>. A shader can only be used in the
   * context where it was compiled. The `copyToContext()` method compiles the
   * shader again and copies it to another drawing context where it can be
   * reused.
   *
   * The parameter, `context`, is the drawing context where the shader will be
   * used. The shader can be copied to an instance of
   * <a href="#/p5.Graphics">p5.Graphics</a>, as in
   * `myShader.copyToContext(pg)`. The shader can also be copied from a
   * <a href="#/p5.Graphics">p5.Graphics</a> object to the main canvas using
   * the `p5.instance` variable, as in `myShader.copyToContext(p5.instance)`.
   *
   * Note: A <a href="#/p5.Shader">p5.Shader</a> object created with
   * <a href="#/p5/createShader">createShader()</a>,
   * <a href="#/p5/createFilterShader">createFilterShader()</a>, or
   * <a href="#/p5/loadShader">loadShader()</a>
   * can be used directly with a <a href="#/p5.Framebuffer">p5.Framebuffer</a>
   * object created with
   * <a href="#/p5/createFramebuffer">createFramebuffer()</a>. Both objects
   * have the same context as the main canvas.
   *
   * @param {p5|p5.Graphics} context WebGL context for the copied shader.
   * @returns {p5.Shader} new shader compiled for the target context.
   *
   * @example
   * // Note: A "uniform" is a global variable within a shader program.
   *
   * // Create a string with the vertex shader program.
   * // The vertex shader is called for each vertex.
   * let vertSrc = `
   * precision highp float;
   * uniform mat4 uModelViewMatrix;
   * uniform mat4 uProjectionMatrix;
   *
   * attribute vec3 aPosition;
   * attribute vec2 aTexCoord;
   * varying vec2 vTexCoord;
   *
   * void main() {
   *   vTexCoord = aTexCoord;
   *   vec4 positionVec4 = vec4(aPosition, 1.0);
   *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
   * }
   * `;
   *
   * // Create a string with the fragment shader program.
   * // The fragment shader is called for each pixel.
   * let fragSrc = `
   * precision mediump float;
   * varying vec2 vTexCoord;
   *
   * void main() {
   *   vec2 uv = vTexCoord;
   *   vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0));
   *   gl_FragColor = vec4(color, 1.0);\
   * }
   * `;
   *
   * let pg;
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   background(200);
   *
   *   // Create a p5.Shader object.
   *   let original = createShader(vertSrc, fragSrc);
   *
   *   // Compile the p5.Shader object.
   *   shader(original);
   *
   *   // Create a p5.Graphics object.
   *   pg = createGraphics(50, 50, WEBGL);
   *
   *   // Copy the original shader to the p5.Graphics object.
   *   let copied = original.copyToContext(pg);
   *
   *   // Apply the copied shader to the p5.Graphics object.
   *   pg.shader(copied);
   *
   *   // Style the display surface.
   *   pg.noStroke();
   *
   *   // Add a display surface for the shader.
   *   pg.plane(50, 50);
   *
   *   describe('A square with purple-blue gradient on its surface drawn against a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Draw the p5.Graphics object to the main canvas.
   *   image(pg, -25, -25);
   * }
   *
   * @example
   * // Note: A "uniform" is a global variable within a shader program.
   *
   * // Create a string with the vertex shader program.
   * // The vertex shader is called for each vertex.
   * let vertSrc = `
   * precision highp float;
   * uniform mat4 uModelViewMatrix;
   * uniform mat4 uProjectionMatrix;
   *
   * attribute vec3 aPosition;
   * attribute vec2 aTexCoord;
   * varying vec2 vTexCoord;
   *
   * void main() {
   *   vTexCoord = aTexCoord;
   *   vec4 positionVec4 = vec4(aPosition, 1.0);
   *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
   * }
   * `;
   *
   * // Create a string with the fragment shader program.
   * // The fragment shader is called for each pixel.
   * let fragSrc = `
   * precision mediump float;
   *
   * varying vec2 vTexCoord;
   *
   * void main() {
   *   vec2 uv = vTexCoord;
   *   vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0));
   *   gl_FragColor = vec4(color, 1.0);
   * }
   * `;
   *
   * let copied;
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   // Create a p5.Graphics object.
   *   let pg = createGraphics(25, 25, WEBGL);
   *
   *   // Create a p5.Shader object.
   *   let original = pg.createShader(vertSrc, fragSrc);
   *
   *   // Compile the p5.Shader object.
   *   pg.shader(original);
   *
   *   // Copy the original shader to the main canvas.
   *   copied = original.copyToContext(p5.instance);
   *
   *   // Apply the copied shader to the main canvas.
   *   shader(copied);
   *
   *   describe('A rotating cube with a purple-blue gradient on its surface drawn against a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Rotate around the x-, y-, and z-axes.
   *   rotateX(frameCount * 0.01);
   *   rotateY(frameCount * 0.01);
   *   rotateZ(frameCount * 0.01);
   *
   *   // Draw the box.
   *   box(50);
   * }
   */
  copyToContext(context) {
    const args = [context._renderer];
    if (this.shaderType === 'compute') {
      args.push(this._computeSrc);
    } else {
      args.push(this._vertSrc, this._fragSrc);
    }
    args.push(this.hooks);
    const shader = new Shader(...args);
    shader.ensureCompiledOnContext(context._renderer);
    return shader;
  }

  /**
   * @private
   */
  ensureCompiledOnContext(context) {
    if (this._compiled && this._renderer !== context) {
      throw new Error(
        'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?'
      );
    } else if (!this._compiled) {
      this._renderer = context?._renderer?.filterRenderer?._renderer || context;
      this.init();
    }
  }

  /**
   * Queries the active attributes for this shader and loads
   * their names and locations into the attributes array.
   * @private
   */
  _loadAttributes() {
    if (this._loadedAttributes) {
      return;
    }
    this.attributes = this._renderer._getShaderAttributes(this);
    this._loadedAttributes = true;
  }

  /**
   * Queries the active uniforms for this shader and loads
   * their names and locations into the uniforms array.
   * @private
   */
  _loadUniforms() {
    if (this._loadedUniforms) {
      return;
    }

    this.uniforms = {};
    this.samplers = [];

    const uniformMetadata = this._renderer.getUniformMetadata(this);

    for (const meta of uniformMetadata) {
      const uniform = {
        ...meta,
        _cachedData: undefined,
      };

      if (uniform.isSampler) {
        this.samplers.push(uniform);
      }

      this.uniforms[uniform.name] = uniform;
    }

    this._loadedUniforms = true;
  }

  /**
   * initializes (if needed) and binds the shader program.
   * @private
   */
  bindShader(shaderType, options) {
    if (this.shaderType && this.shaderType !== shaderType) {
      throw new Error(
        `You've already used this shader as a ${this.shaderType} shader, but are now using it as a ${shaderType}.`
      );
    }
    this.shaderType = shaderType;
    this.init();
    if (!this._bound) {
      this.useProgram(options);
      this._bound = true;
    }
  }

  /**
   * @chainable
   * @private
   */
  unbindShader() {
    if (this._bound) {
      this.unbindTextures();
      this._bound = false;
    }
    return this;
  }

  /**
   * @private
   */
  bindTextures() {
    const empty = this._renderer._getEmptyTexture();

    for (const uniform of this.samplers) {
      if (uniform.noData) continue;
      let tex = uniform.texture;
      if (
        tex === undefined ||
        (
          // Make sure we unbind a framebuffer uniform if it's the same
          // framebuffer that is actvely being drawn to in order to
          // prevent a feedback cycle
          tex.isFramebufferTexture &&
          !tex.src.framebuffer.antialias &&
          tex.src.framebuffer === this._renderer.activeFramebuffer()
        )
      ) {
        // user hasn't yet supplied a texture for this slot.
        // (or there may not be one--maybe just lighting),
        // so we supply a default texture instead.
        uniform.texture = tex = empty;
      }
      this._renderer._updateTexture(uniform, tex);
    }
  }

  /**
   * @private
   */
  unbindTextures() {
    for (const uniform of this.samplers) {
      if (uniform.texture?.isFramebufferTexture) {
        this._renderer._unbindFramebufferTexture(uniform);
      }
    }
  }

  /**
   * @chainable
   * @private
   */
  useProgram(options) {
    if (this._renderer._curShader !== this) {
      this._renderer._useShader(this);
      this._renderer._curShader = this;
    }
    return this;
  }

  /**
   * Sets the shader’s uniform (global) variables.
   *
   * Shader programs run on the computer’s graphics processing unit (GPU).
   * They live in part of the computer’s memory that’s completely separate
   * from the sketch that runs them. Uniforms are global variables within a
   * shader program. They provide a way to pass values from a sketch running
   * on the CPU to a shader program running on the GPU.
   *
   * The first parameter, `uniformName`, is a string with the uniform’s name.
   * For the shader above, `uniformName` would be `'r'`.
   *
   * The second parameter, `data`, is the value that should be used to set the
   * uniform. For example, calling `myShader.setUniform('r', 0.5)` would set
   * the `r` uniform in the shader above to `0.5`. data should match the
   * uniform’s type. Numbers, strings, booleans, arrays, and many types of
   * images can all be passed to a shader with `setUniform()`.
   *
   * @chainable
   * @param {String} uniformName name of the uniform. Must match the name
   *                             used in the vertex and fragment shaders.
   * @param {Boolean|p5.Vector|p5.Color|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture|p5.StorageBuffer}
   * data value to assign to the uniform. Must match the uniform’s data type.
   *
   * @example
   * // Note: A "uniform" is a global variable within a shader program.
   *
   * // Create a string with the vertex shader program.
   * // The vertex shader is called for each vertex.
   * let vertSrc = `
   * precision highp float;
   * uniform mat4 uModelViewMatrix;
   * uniform mat4 uProjectionMatrix;
   *
   * attribute vec3 aPosition;
   * attribute vec2 aTexCoord;
   * varying vec2 vTexCoord;
   *
   * void main() {
   *   vTexCoord = aTexCoord;
   *   vec4 positionVec4 = vec4(aPosition, 1.0);
   *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
   * }
   * `;
   *
   * // Create a string with the fragment shader program.
   * // The fragment shader is called for each pixel.
   * let fragSrc = `
   * precision mediump float;
   *
   * uniform float r;
   *
   * void main() {
   *   gl_FragColor = vec4(r, 1.0, 1.0, 1.0);
   * }
   * `;
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   // Create a p5.Shader object.
   *   let myShader = createShader(vertSrc, fragSrc);
   *
   *   // Apply the p5.Shader object.
   *   shader(myShader);
   *
   *   // Set the r uniform to 0.5.
   *   myShader.setUniform('r', 0.5);
   *
   *   // Style the drawing surface.
   *   noStroke();
   *
   *   // Add a plane as a drawing surface for the shader.
   *   plane(100, 100);
   *
   *   describe('A cyan square.');
   * }
   *
   * @example
   * // Note: A "uniform" is a global variable within a shader program.
   *
   * // Create a string with the vertex shader program.
   * // The vertex shader is called for each vertex.
   * let vertSrc = `
   * precision highp float;
   * uniform mat4 uModelViewMatrix;
   * uniform mat4 uProjectionMatrix;
   *
   * attribute vec3 aPosition;
   * attribute vec2 aTexCoord;
   * varying vec2 vTexCoord;
   *
   * void main() {
   *   vTexCoord = aTexCoord;
   *   vec4 positionVec4 = vec4(aPosition, 1.0);
   *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
   * }
   * `;
   *
   * // Create a string with the fragment shader program.
   * // The fragment shader is called for each pixel.
   * let fragSrc = `
   * precision mediump float;
   *
   * uniform float r;
   *
   * void main() {
   *   gl_FragColor = vec4(r, 1.0, 1.0, 1.0);
   * }
   * `;
   *
   * let myShader;
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   // Create a p5.Shader object.
   *   myShader = createShader(vertSrc, fragSrc);
   *
   *   // Compile and apply the p5.Shader object.
   *   shader(myShader);
   *
   *   describe('A square oscillates color between cyan and white.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Style the drawing surface.
   *   noStroke();
   *
   *   // Update the r uniform.
   *   let nextR = 0.5 * (sin(frameCount * 0.01) + 1);
   *   myShader.setUniform('r', nextR);
   *
   *   // Add a plane as a drawing surface.
   *   plane(100, 100);
   * }
   *
   * @example
   * // Note: A "uniform" is a global variable within a shader program.
   *
   * // Create a string with the vertex shader program.
   * // The vertex shader is called for each vertex.
   * let vertSrc = `
   * precision highp float;
   * uniform mat4 uModelViewMatrix;
   * uniform mat4 uProjectionMatrix;
   *
   * attribute vec3 aPosition;
   * attribute vec2 aTexCoord;
   * varying vec2 vTexCoord;
   *
   * void main() {
   *   vTexCoord = aTexCoord;
   *   vec4 positionVec4 = vec4(aPosition, 1.0);
   *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
   * }
   * `;
   *
   * // Create a string with the fragment shader program.
   * // The fragment shader is called for each pixel.
   * let fragSrc = `
   * precision highp float;
   * uniform vec2 p;
   * uniform float r;
   * const int numIterations = 500;
   * varying vec2 vTexCoord;
   *
   * void main() {
   *   vec2 c = p + gl_FragCoord.xy * r;
   *   vec2 z = c;
   *   float n = 0.0;
   *
   *   for (int i = numIterations; i > 0; i--) {
   *     if (z.x * z.x + z.y * z.y > 4.0) {
   *       n = float(i) / float(numIterations);
   *       break;
   *     }
   *
   *     z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;
   *   }
   *
   *   gl_FragColor = vec4(
   *     0.5 - cos(n * 17.0) / 2.0,
   *     0.5 - cos(n * 13.0) / 2.0,
   *     0.5 - cos(n * 23.0) / 2.0,
   *     1.0
   *   );
   * }
   * `;
   *
   * let mandelbrot;
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   // Create a p5.Shader object.
   *   mandelbrot = createShader(vertSrc, fragSrc);
   *
   *   // Compile and apply the p5.Shader object.
   *   shader(mandelbrot);
   *
   *   // Set the shader uniform p to an array.
   *   // p is the center point of the Mandelbrot image.
   *   mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]);
   *
   *   describe('A fractal image zooms in and out of focus.');
   * }
   *
   * function draw() {
   *   // Set the shader uniform r to a value that oscillates
   *   // between 0 and 0.005.
   *   // r is the size of the image in Mandelbrot-space.
   *   let radius = 0.005 * (sin(frameCount * 0.01) + 1);
   *   mandelbrot.setUniform('r', radius);
   *
   *   // Style the drawing surface.
   *   noStroke();
   *
   *   // Add a plane as a drawing surface.
   *   plane(100, 100);
   * }
   */
  setUniform(uniformName, data) {
    this.init();

    const resolvedName =
      this.hooks.shaderNameMap?.externalToInternal?.[uniformName] ??
      uniformName;
    const uniform = this.uniforms[resolvedName];
    if (!uniform) {
      return;
    }

    if (uniformName === 'uSampler' && !this._renderer._settingFillUniforms) {
      this._userSetSampler = true;
    }

    // In p5.strands-related code, where some of the code may be in
    // p5.webgpu.js instead of the main p5.js build, we generally use
    // duck typing instead of instanceof to avoid accidentally importing
    // and comparing against a separate copy of p5 classes
    if (data?.isVector) {
      data = data.values.length !== data.dimensions ? data.values.slice(0, data.dimensions) : data.values;
    } else if (data?.isColor) {
      data = data._getRGBA([1, 1, 1, 1]);
    }

    if (uniform.isArray) {
      if (
        uniform._cachedData &&
        this._renderer._arraysEqual(uniform._cachedData, data)
      ) {
        return;
      } else {
        uniform._cachedData = data.slice(0);
      }
    } else if (uniform._cachedData && uniform._cachedData === data) {
      return;
    } else {
      if (Array.isArray(data) || data instanceof TypedArray) {
        if (uniform._cachedData && this._renderer._arraysEqual(uniform._cachedData, data)) {
          return;
        }
        uniform._cachedData = data.slice(0);
      } else {
        if (uniform._cachedData === data) return;
        uniform._cachedData = data;
      }
    }

    this._renderer.updateUniformValue(this, uniform, data);
  }

  /**
   * @chainable
   * @private
   */
  enableAttrib(attr, size, type, normalized, stride, offset) {
    if (attr) {
      if (
        typeof IS_MINIFIED === 'undefined' &&
        this.attributes[attr.name] !== attr
      ) {
        console.warn(
          `The attribute "${attr.name}"passed to enableAttrib does not belong to this shader.`
        );
      }

      if (attr.location !== -1) {
        this._renderer._enableAttrib(this, attr, size, type, normalized, stride, offset);
      }
    }
    return this;
  }
};

function shader(p5, fn){
  /**
   * A class to describe a shader program.
   *
   * Each `p5.Shader` object contains a shader program that runs on the graphics
   * processing unit (GPU). Shaders can process many pixels or vertices at the
   * same time, making them fast for many graphics tasks. They’re written in a
   * language called
   * <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders" target="_blank">GLSL</a>
   * and run along with the rest of the code in a sketch.
   *
   * A shader program consists of two files, a vertex shader and a fragment
   * shader. The vertex shader affects where 3D geometry is drawn on the screen
   * and the fragment shader affects color. Once the `p5.Shader` object is
   * created, it can be used with the <a href="#/p5/shader">shader()</a>
   * function, as in `shader(myShader)`.
   *
   * A shader can optionally describe *hooks,* which are functions in GLSL that
   * users may choose to provide to customize the behavior of the shader. For the
   * vertex or the fragment shader, users can pass in an object where each key is
   * the type and name of a hook function, and each value is a string with the
   * parameter list and default implementation of the hook. For example, to let users
   * optionally run code at the start of the vertex shader, the options object could
   * include:
   *
   * ```js
   * {
   *   vertex: {
   *     'void beforeVertex': '() {}'
   *   }
   * }
   * ```
   *
   * Then, in your vertex shader source, you can run a hook by calling a function
   * with the same name prefixed by `HOOK_`:
   *
   * ```glsl
   * void main() {
   *   HOOK_beforeVertex();
   *   // Add the rest ofy our shader code here!
   * }
   * ```
   *
   * Note: <a href="#/p5/createShader">createShader()</a>,
   * <a href="#/p5/createFilterShader">createFilterShader()</a>, and
   * <a href="#/p5/loadShader">loadShader()</a> are the recommended ways to
   * create an instance of this class.
   *
   * @class p5.Shader
   * @constructor
   * @param {p5.RendererGL} renderer WebGL context for this shader.
   * @param {String} vertSrc source code for the vertex shader program.
   * @param {String} fragSrc source code for the fragment shader program.
   * @param {Object} [options] An optional object describing how this shader can
   * be augmented with hooks. It can include:
   *  - `vertex`: An object describing the available vertex shader hooks.
   *  - `fragment`: An object describing the available frament shader hooks.
   *
   * @example
   * // Note: A "uniform" is a global variable within a shader program.
   *
   * // Create a string with the vertex shader program.
   * // The vertex shader is called for each vertex.
   * let vertSrc = `
   * precision highp float;
   * uniform mat4 uModelViewMatrix;
   * uniform mat4 uProjectionMatrix;
   *
   * attribute vec3 aPosition;
   * attribute vec2 aTexCoord;
   * varying vec2 vTexCoord;
   *
   * void main() {
   *   vTexCoord = aTexCoord;
   *   vec4 positionVec4 = vec4(aPosition, 1.0);
   *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
   * }
   * `;
   *
   * // Create a string with the fragment shader program.
   * // The fragment shader is called for each pixel.
   * let fragSrc = `
   * precision highp float;
   *
   * void main() {
   *   // Set each pixel's RGBA value to yellow.
   *   gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
   * }
   * `;
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   // Create a p5.Shader object.
   *   let myShader = createShader(vertSrc, fragSrc);
   *
   *   // Apply the p5.Shader object.
   *   shader(myShader);
   *
   *   // Style the drawing surface.
   *   noStroke();
   *
   *   // Add a plane as a drawing surface.
   *   plane(100, 100);
   *
   *   describe('A yellow square.');
   * }
   *
   * @example
   * // Note: A "uniform" is a global variable within a shader program.
   *
   * let mandelbrot;
   *
   * async function setup() {
   *   mandelbrot = await loadShader('assets/shader.vert', 'assets/shader.frag');
   *   createCanvas(100, 100, WEBGL);
   *
   *   // Use the p5.Shader object.
   *   shader(mandelbrot);
   *
   *   // Set the shader uniform p to an array.
   *   mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]);
   *
   *   describe('A fractal image zooms in and out of focus.');
   * }
   *
   * function draw() {
   *   // Set the shader uniform r to a value that oscillates between 0 and 2.
   *   mandelbrot.setUniform('r', sin(frameCount * 0.01) + 1);
   *
   *   // Add a quad as a display surface for the shader.
   *   quad(-1, -1, 1, -1, 1, 1, -1, 1);
   * }
   */
  p5.Shader = Shader;
}

export default shader;
export { Shader };

if(typeof p5 !== 'undefined'){
  shader(p5, p5.prototype);
}
