/* eslint-disable no-inline-comments */
import GL from '../constants';
import VertexArray from './vertex-array';
import Resource from './resource';
import Texture from './texture';
import Framebuffer from './framebuffer';
import { parseUniformName, getUniformSetter } from './uniforms';
import { VertexShader, FragmentShader } from './shader';
import Buffer from './buffer';
import { withParameters } from '../webgl-context/context-state';
import { assertWebGL2Context, isWebGL2 } from '../webgl-utils';
import { getPrimitiveDrawMode } from '../webgl-utils/attribute-utils';
import { log, uid, isObjectEmpty } from '../utils';
import assert from '../utils/assert';
const LOG_PROGRAM_PERF_PRIORITY = 3; // const GL_TRANSFORM_FEEDBACK_BUFFER_MODE = 0x8C7F;
// const GL_TRANSFORM_FEEDBACK_VARYINGS = 0x8C83;
// MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS : 0x8C80,
// TRANSFORM_FEEDBACK_BUFFER_START: 0x8C84,
// TRANSFORM_FEEDBACK_BUFFER_SIZE : 0x8C85,
// TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN: 0x8C88,
// MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS: 0x8C8A,
// MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS: 0x8C8B,

const GL_INTERLEAVED_ATTRIBS = 0x8C8C;
const GL_SEPARATE_ATTRIBS = 0x8C8D;
export default class Program extends Resource {
  constructor(gl, opts = {}) {
    super(gl, opts);
    this.initialize(opts);
    this.vertexAttributes = VertexArray.getDefaultArray(gl);
    Object.seal(this);

    this._setId(opts.id);
  }

  initialize({
    vs,
    fs,
    defaultUniforms,
    varyings,
    bufferMode = GL_SEPARATE_ATTRIBS
  } = {}) {
    // Create shaders if needed
    this.vs = typeof vs === 'string' ? new VertexShader(this.gl, vs) : vs;
    this.fs = typeof fs === 'string' ? new FragmentShader(this.gl, fs) : fs;
    assert(this.vs instanceof VertexShader, 'Program: bad vertex shader');
    assert(this.fs instanceof FragmentShader, 'Program: bad fragment shader');
    this.defaultUniforms = defaultUniforms; // Setup varyings if supplied

    if (varyings) {
      assertWebGL2Context(this.gl);
      this.varyings = varyings;
      this.gl.transformFeedbackVaryings(this.handle, varyings, bufferMode);
      this.varyingMap = getVaryingMap(varyings, bufferMode);
    } else {
      this.varyingMap = {};
    }

    this._compileAndLink(); // Experimental flag to avoid deleting Program object while it is cached


    this._isCached = false;
    return this;
  } // Generates warning if a vertex shader attribute is not setup.


  checkAttributeBindings({
    vertexArray
  }) {
    const filledLocations = vertexArray ? vertexArray.filledLocations : this.vertexAttributes.filledLocations;

    for (const attributeName in this._attributeToLocationMap) {
      const location = this._attributeToLocationMap[attributeName];

      if (!filledLocations[location] && !this._warnedLocations[location]) {
        // throw new Error(`Program ${this.id}: ` +
        //   `Attribute ${location}:${attributeName} not supplied`);
        log.warn(`Program ${this.id}: Attribute ${location}:${attributeName} not supplied`)();
        this._warnedLocations[location] = true;
      }
    }

    return this;
  }

  delete(opts = {}) {
    if (this._isCached) {
      // This object is cached, do not delete
      return this;
    }

    return super.delete(opts);
  }

  reset() {
    this.unsetBuffers(); // TODO - reset uniforms and attributes to initial state
  }

  use() {
    this.gl.useProgram(this.handle);
    return this;
  } // A good thing about webGL is that there are so many ways to draw things,
  // e.g. depending on whether data is indexed and/or isInstanced.
  // This function unifies those into a single call with simple parameters
  // that have sane defaults.


  draw({
    drawMode = GL.TRIANGLES,
    vertexCount,
    offset = 0,
    start,
    end,
    isIndexed = false,
    indexType = GL.UNSIGNED_SHORT,
    isInstanced = false,
    instanceCount = 0,
    vertexArray = null,
    transformFeedback = null,
    uniforms = {},
    samplers = {},
    parameters = {}
  }) {
    vertexArray = vertexArray || VertexArray.getDefaultArray(this.gl);
    vertexArray.bind(() => {
      this.gl.useProgram(this.handle);

      if (transformFeedback) {
        const primitiveMode = getPrimitiveDrawMode(drawMode);
        transformFeedback.begin(primitiveMode);
      }

      this.setUniforms(uniforms, samplers);
      withParameters(this.gl, parameters, () => {
        // TODO - Use polyfilled WebGL2RenderingContext instead of ANGLE extension
        if (isIndexed && isInstanced) {
          this.ext.drawElementsInstanced(drawMode, vertexCount, indexType, offset, instanceCount);
        } else if (isIndexed && isWebGL2(this.gl) && !isNaN(start) && !isNaN(end)) {
          this.gl.drawElementsRange(drawMode, start, end, vertexCount, indexType, offset);
        } else if (isIndexed) {
          this.gl.drawElements(drawMode, vertexCount, indexType, offset);
        } else if (isInstanced) {
          this.ext.drawArraysInstanced(drawMode, offset, vertexCount, instanceCount);
        } else {
          this.gl.drawArrays(drawMode, offset, vertexCount);
        }
      }); // this.gl.useProgram(null);

      if (transformFeedback) {
        transformFeedback.end();
      }
    });
    return this;
  }
  /**
   * Attach a map of Buffers values to a program
   * Only attributes with names actually present in the linked program
   * will be updated. Other supplied buffers will be ignored.
   *
   * @param {Object} buffers - An object map with attribute names being keys
   *  and values are expected to be instances of Buffer.
   * @returns {Program} Returns itself for chaining.
   */

  /* eslint-disable max-statements */


  setBuffers(buffers, {
    clear = true,
    drawParams = {}
  } = {}) {
    if (clear) {
      this.vertexAttributes.clearBindings();
    } // indexing is autodetected - buffer with target gl.ELEMENT_ARRAY_BUFFER
    // index type is saved for drawElement calls


    drawParams.isInstanced = false;
    drawParams.isIndexed = false;
    drawParams.indexType = null;

    const _sortBuffersByLocatio = this._sortBuffersByLocation(buffers),
          locations = _sortBuffersByLocatio.locations,
          elements = _sortBuffersByLocatio.elements; // Process locations in order


    for (let location = 0; location < locations.length; ++location) {
      const bufferName = locations[location];
      const buffer = buffers[bufferName]; // DISABLE MISSING ATTRIBUTE

      if (!buffer) {
        this.vertexAttributes.disable(location);
      } else if (buffer instanceof Buffer) {
        const divisor = buffer.layout.instanced ? 1 : 0;
        this.vertexAttributes.setBuffer({
          location,
          buffer
        });
        this.vertexAttributes.setDivisor(location, divisor);
        drawParams.isInstanced = buffer.layout.instanced > 0;
        this.vertexAttributes.enable(location);
      } else {
        this.vertexAttributes.setGeneric({
          location,
          array: buffer
        });
        this.vertexAttributes.disable(location, true);
      }
    } // SET ELEMENTS ARRAY BUFFER


    if (elements) {
      const buffer = buffers[elements];
      buffer.bind();
      drawParams.isIndexed = true;
      drawParams.indexType = buffer.layout.type;
    }

    return this;
  }
  /* eslint-enable max-statements */

  /*
   * @returns {Program} Returns itself for chaining.
   */


  unsetBuffers() {
    const length = this._attributeCount;

    for (let i = 1; i < length; ++i) {
      // this.vertexAttributes.setDivisor(i, 0);
      this.vertexAttributes.disable(i);
    } // Clear elements buffer


    this.gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, null);
    return this;
  }
  /**
   * Apply a set of uniform values to a program
   * Only uniforms with names actually present in the linked program
   * will be updated.
   * other uniforms will be ignored
   *
   * @param {Object} uniformMap - An object with names being keys
   * @returns {Program} - returns itself for chaining.
   */

  /* eslint-disable max-depth */


  setUniforms(uniforms, samplers = {}) {
    for (const uniformName in uniforms) {
      let uniform = uniforms[uniformName];
      const uniformSetter = this._uniformSetters[uniformName];
      const sampler = samplers[uniformName];

      if (uniformSetter) {
        if (uniform instanceof Framebuffer) {
          uniform = uniform.texture;
        }

        if (uniform instanceof Texture) {
          if (uniformSetter.textureIndex === undefined) {
            uniformSetter.textureIndex = this._textureIndexCounter++;
          } // Bind texture to index


          const texture = uniform;
          const textureIndex = uniformSetter.textureIndex;
          texture.bind(textureIndex); // Bind a sampler (if supplied) to index

          if (sampler) {
            sampler.bind(textureIndex);
          } // Set the uniform sampler to the texture index


          uniformSetter(textureIndex);
        } else {
          // Just set the value
          uniformSetter(uniform);
        }
      }
    }

    return this;
  }
  /* eslint-enable max-depth */
  // Binds a uniform block (`blockIndex`) to a specific binding point (`blockBinding`)


  uniformBlockBinding(blockIndex, blockBinding) {
    assertWebGL2Context(this.gl);
    this.gl.uniformBlockBinding(this.handle, blockIndex, blockBinding);
  } // setTransformFeedbackBuffers(buffers) {
  //   for (const buffer of buffers) {
  //     buffer.bindBase()
  //   }
  // }

  /**
   * ATTRIBUTES API
   * (Locations are numeric indices)
   * @return {Number} count
   */


  getAttributeCount() {
    return this._getParameter(GL.ACTIVE_ATTRIBUTES);
  }
  /**
   * Returns location (index) of a name
   * @param {String} attributeName - name of an attribute
   *   (matches name in a linked shader)
   * @returns {Number} - // array of actual attribute names from shader linking
   */


  getAttributeLocation(attributeName) {
    return this.gl.getAttribLocation(this.handle, attributeName);
  }
  /**
   * Returns an object with info about attribute at index "location"/
   * @param {int} location - index of an attribute
   * @returns {WebGLActiveInfo} - info about an active attribute
   *   fields: {name, size, type}
   */


  getAttributeInfo(location) {
    return this.gl.getActiveAttrib(this.handle, location);
  }
  /**
   * UNIFORMS API
   * (Locations are numeric indices)
   * @return {Number} count
   */


  getUniformCount() {
    return this._getParameter(GL.ACTIVE_UNIFORMS);
  }
  /*
   * @returns {WebGLActiveInfo} - object with {name, size, type}
   */


  getUniformInfo(index) {
    return this.gl.getActiveUniform(this.handle, index);
  }
  /*
   * @returns {WebGLUniformLocation} - opaque object representing location
   * of uniform, used by setter methods
   */


  getUniformLocation(name) {
    return this.gl.getUniformLocation(this.handle, name);
  }

  getUniformValue(location) {
    return this.gl.getUniform(this.handle, location);
  }
  /* eslint-disable max-len */
  // Rretrieves information about active uniforms identifed by their indices (`uniformIndices`)
  // For valid `pname` values check :
  // https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/getActiveUniforms


  getActiveUniforms(uniformIndices, pname) {
    return this.gl.getActiveUniforms(this.handle, uniformIndices, pname);
  }
  /* eslint-enable max-len */
  // WebGL2

  /**
   * @param {GLuint} index
   * @return {WebGLActiveInfo} - object with {name, size, type}
   */


  getVarying(program, index) {
    const result = this.gl.getTransformFeedbackVarying(program, index);
    return result;
  } // Retrieves the assigned color number binding for the user-defined varying
  // out variable name for program. program must have previously been linked.


  getFragDataLocation(varyingName) {
    assertWebGL2Context(this.gl);
    return this.gl.getFragDataLocation(this.handle, varyingName);
  } // @returns {WebGLShader[]} - array of attached WebGLShader objects


  getAttachedShaders() {
    return this.gl.getAttachedShaders(this.handle);
  } // Retrieves the index of a uniform block


  getUniformBlockIndex(blockName) {
    assertWebGL2Context(this.gl);
    return this.gl.getUniformBlockIndex(this.handle, blockName);
  }
  /* eslint-disable max-len */
  // Retrieves information about an active uniform block (`blockIndex`)
  // For valid `pname` values check :
  // https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/getActiveUniformBlockParameter


  getActiveUniformBlockParameter(blockIndex, pname) {
    assertWebGL2Context(this.gl);
    return this.gl.getActiveUniformBlockParameter(this.handle, blockIndex, pname);
  }
  /* eslint-enable max-len */
  // PRIVATE METHODS


  _compileAndLink() {
    const gl = this.gl;
    gl.attachShader(this.handle, this.vs.handle);
    gl.attachShader(this.handle, this.fs.handle);
    log.time(LOG_PROGRAM_PERF_PRIORITY, `linkProgram for ${this._getName()}`)();
    gl.linkProgram(this.handle);
    log.timeEnd(LOG_PROGRAM_PERF_PRIORITY, `linkProgram for ${this._getName()}`)(); // Avoid checking program linking error in production

    if (gl.debug || log.priority > 0) {
      gl.validateProgram(this.handle);
      const linked = gl.getProgramParameter(this.handle, gl.LINK_STATUS);

      if (!linked) {
        throw new Error(`Error linking ${gl.getProgramInfoLog(this.handle)}`);
      }
    }

    this._queryAttributeLocations();

    this._queryUniformLocations();
  }

  _sortBuffersByLocation(buffers) {
    let elements = null;
    let locations = []; // Reutrn early if no buffers to be bound.

    if (isObjectEmpty(buffers)) {
      return {
        locations,
        elements
      };
    }

    locations = new Array(this._attributeCount);

    for (const bufferName in buffers) {
      const buffer = buffers[bufferName];
      const location = this._attributeToLocationMap[bufferName];

      if (location === undefined) {
        if (buffer.target === GL.ELEMENT_ARRAY_BUFFER && elements) {
          throw new Error(`${this._print(bufferName)} duplicate GL.ELEMENT_ARRAY_BUFFER`);
        } else if (buffer.target === GL.ELEMENT_ARRAY_BUFFER) {
          elements = bufferName;
        } else if (!this._warnedLocations[location]) {
          log.log(2, `${this._print(bufferName)} not used`)();
          this._warnedLocations[location] = true;
        }
      } else {
        if (buffer.target === GL.ELEMENT_ARRAY_BUFFER) {
          throw new Error(`${this._print(bufferName)}:${location} ` + 'has both location and type gl.ELEMENT_ARRAY_BUFFER');
        }

        locations[location] = bufferName;
      }
    }

    return {
      locations,
      elements
    };
  } // Check that all active attributes are enabled


  _areAllAttributesEnabled() {
    const length = this._attributeCount;

    for (let i = 0; i < length; ++i) {
      if (!this.vertexAttributes.isEnabled(i)) {
        return false;
      }
    }

    return true;
  }

  _print(bufferName) {
    return `Program ${this.id}: Attribute ${bufferName}`;
  }

  _createHandle() {
    return this.gl.createProgram();
  }

  _deleteHandle() {
    this.gl.deleteProgram(this.handle);
  }

  _getName() {
    let programName = this.vs.getName() || this.fs.getName();
    programName = programName.replace(/shader/i, '');
    programName = programName ? `${programName}-program` : 'program';
    return programName;
  }

  _getOptionsFromHandle(handle) {
    const shaderHandles = this.gl.getAttachedShaders(handle);
    const opts = {};

    for (const shaderHandle of shaderHandles) {
      const type = this.gl.getShaderParameter(this.handle, GL.SHADER_TYPE);

      switch (type) {
        case GL.VERTEX_SHADER:
          opts.vs = new VertexShader({
            handle: shaderHandle
          });
          break;

        case GL.FRAGMENT_SHADER:
          opts.fs = new FragmentShader({
            handle: shaderHandle
          });
          break;

        default:
      }
    }

    return opts;
  }

  _getParameter(pname) {
    return this.gl.getProgramParameter(this.handle, pname);
  } // query attribute locations and build name to location map.


  _queryAttributeLocations() {
    this._attributeToLocationMap = {};
    this._attributeCount = this.getAttributeCount();

    for (let location = 0; location < this._attributeCount; location++) {
      const name = this.getAttributeInfo(location).name;
      this._attributeToLocationMap[name] = this.getAttributeLocation(name);
    }

    this._warnedLocations = {};
  } // query uniform locations and build name to setter map.


  _queryUniformLocations() {
    const gl = this.gl;
    this._uniformSetters = {};
    this._uniformCount = this.getUniformCount();

    for (let i = 0; i < this._uniformCount; i++) {
      const info = this.getUniformInfo(i);
      const parsedName = parseUniformName(info.name);
      const location = this.getUniformLocation(parsedName.name);
      this._uniformSetters[parsedName.name] = getUniformSetter(gl, location, info, parsedName.isArray);
    }

    this._textureIndexCounter = 0;
  }

  _setId(id) {
    // If program is not named, name it after shader names
    if (!id) {
      const programName = this._getName(); // TODO - this.id will already have been initialized


      this.id = uid(programName);
    }
  }

} // create uniform setters
// Map of uniform names to setter functions

export function getUniformDescriptors(gl, program) {
  const uniformDescriptors = {};
  const length = program.getUniformCount();

  for (let i = 0; i < length; i++) {
    const info = program.getUniformInfo(i);
    const location = program.getUniformLocation(info.name);
    const descriptor = getUniformSetter(gl, location, info);
    uniformDescriptors[descriptor.name] = descriptor;
  }

  return uniformDescriptors;
} // Get a map of buffer indices

export function getVaryingMap(varyings, bufferMode) {
  const varyingMap = {};
  let index = 0;
  assert(bufferMode === GL_SEPARATE_ATTRIBS || bufferMode === GL_INTERLEAVED_ATTRIBS);
  const indexIncrement = bufferMode === GL_SEPARATE_ATTRIBS ? 1 : 0;

  for (const varying of varyings) {
    varyingMap[varying] = index;
    index += indexIncrement;
  }

  return varyingMap;
}
//# sourceMappingURL=program.js.map