'use strict';

var constants = require('../../constants.js');
var extensions = require('../../extensions.js');
var settings = require('../../settings/settings.js');
var Geometry = require('./Geometry.js');
var BufferCopyOperation = require('./BufferCopyOperation.js');

const byteSizeMap = {
  5126: 4,
  5123: 2,
  5121: 1,
  5125: 4,
  5124: 4,
  5122: 2,
  5120: 1
};
class GeometrySystem {
  /** @param renderer - The renderer this System works for. */
  constructor(renderer) {
    this.renderer = renderer;
    this._activeGeometry = null;
    this._activeGPS = null;
    this._activeBB = null;
    this.hasVao = true;
    this.hasInstance = true;
    this.canUseUInt32ElementIndex = false;
    this.managedGeometries = {};
  }
  /** Sets up the renderer context and necessary buffers. */
  contextChange() {
    this.disposeAll(true);
    const gl = this.gl = this.renderer.gl;
    const context = this.renderer.context;
    this.CONTEXT_UID = this.renderer.CONTEXT_UID;
    if (context.webGLVersion !== 2) {
      let nativeVaoExtension = this.renderer.context.extensions.vertexArrayObject;
      if (settings.settings.PREFER_ENV === constants.ENV.WEBGL_LEGACY) {
        nativeVaoExtension = null;
      }
      if (nativeVaoExtension) {
        gl.createVertexArray = () => nativeVaoExtension.createVertexArrayOES();
        gl.bindVertexArray = (vao) => nativeVaoExtension.bindVertexArrayOES(vao);
        gl.deleteVertexArray = (vao) => nativeVaoExtension.deleteVertexArrayOES(vao);
      } else {
        this.hasVao = false;
        gl.createVertexArray = () => null;
        gl.bindVertexArray = () => null;
        gl.deleteVertexArray = () => null;
      }
    }
    if (context.webGLVersion !== 2) {
      const instanceExt = gl.getExtension("ANGLE_instanced_arrays");
      if (instanceExt) {
        gl.vertexAttribDivisor = (a, b) => instanceExt.vertexAttribDivisorANGLE(a, b);
        gl.drawElementsInstanced = (a, b, c, d, e) => instanceExt.drawElementsInstancedANGLE(a, b, c, d, e);
        gl.drawArraysInstanced = (a, b, c, d) => instanceExt.drawArraysInstancedANGLE(a, b, c, d);
      } else {
        this.hasInstance = false;
      }
    }
    this.canUseUInt32ElementIndex = context.webGLVersion === 2 || !!context.extensions.uint32ElementIndex;
  }
  /**
   * Binds geometry so that is can be drawn. Creating a Vao if required
   * @param geometry - Instance of geometry to bind.
   * @param shader - Instance of shader to use vao for.
   */
  bind(geometry, shader) {
    shader = shader || this.renderer.shader.shader;
    const { gl, CONTEXT_UID } = this;
    let glGeom = geometry.glVertexArrayObjects[CONTEXT_UID];
    if (!glGeom) {
      this.managedGeometries[geometry.id] = geometry;
      geometry.disposeRunner.add(this);
      geometry.glVertexArrayObjects[CONTEXT_UID] = glGeom = new Geometry.GeometryPerGL(CONTEXT_UID);
    } else if (glGeom.bufRefCount === 0) {
      this.regenVao(geometry, glGeom);
    }
    const gps = glGeom.bySignature[shader.program.id] || this.initGeometryVao(geometry, shader, glGeom);
    this._activeGeometry = geometry;
    if (this._activeGPS !== gps) {
      this._activeGPS = gps;
      if (this.hasVao) {
        gl.bindVertexArray(gps.vao);
      } else {
        this.activateVao(geometry, shader.program);
      }
    }
    this.updateBuffers();
    this._activeBB = null;
  }
  /** Reset and unbind any active VAO and geometry. */
  reset() {
    this.unbind();
  }
  /** Update buffers of the currently bound geometry. */
  updateBuffers() {
    const geometry = this._activeGeometry;
    const bufferSystem = this.renderer.buffer;
    for (let i = 0; i < geometry.buffers.length; i++) {
      const buffer = geometry.buffers[i];
      bufferSystem.update(buffer);
    }
  }
  /**
   * Check compatibility between a geometry and a program
   * @param geometry - Geometry instance.
   * @param program - Program instance.
   */
  checkCompatibility(geometry, program) {
    const geometryAttributes = geometry.attributes;
    const shaderAttributes = program.attributeData;
    for (const j in shaderAttributes) {
      if (!geometryAttributes[j]) {
        throw new Error(`shader and geometry incompatible, geometry missing the "${j}" attribute`);
      }
    }
  }
  /**
   * Takes a geometry and program and generates a unique signature for them.
   * @param geometry - To get signature from.
   * @param program - To test geometry against.
   * @returns - Unique signature of the geometry and program
   */
  getSignature(geometry, program) {
    const attribs = geometry.attributes;
    const shaderAttributes = program.attributeData;
    const strings = ["g", geometry.id];
    for (const i in attribs) {
      if (shaderAttributes[i]) {
        strings.push(i, shaderAttributes[i].location);
      }
    }
    return strings.join("-");
  }
  /**
   * Creates or gets Vao with the same structure as the geometry and stores it on the geometry.
   * If vao is created, it is bound automatically. We use a shader to infer what and how to set up the
   * attribute locations.
   * @param geometry - Instance of geometry to generate Vao for.
   * @param shader - Instance of the shader.
   * @param glGeom - geometry-gl object
   */
  initGeometryVao(geometry, shader, glGeom) {
    const gl = this.gl;
    const CONTEXT_UID = this.CONTEXT_UID;
    const bufferSystem = this.renderer.buffer;
    const program = shader.program;
    if (!program.glPrograms[CONTEXT_UID]) {
      this.renderer.shader.generateProgram(shader);
    }
    this.checkCompatibility(geometry, program);
    const signature = this.getSignature(geometry, program);
    let gps = glGeom.bySignature[signature];
    if (gps) {
      glGeom.bySignature[program.id] = gps;
      return gps;
    }
    if (geometry.attributeDirty) {
      this.checkAttributes(geometry, program);
    }
    gps = new Geometry.GeometryPerShader(gl.createVertexArray());
    gl.bindVertexArray(gps.vao);
    this.activateVao(geometry, program);
    this.addRefBuffers(geometry, glGeom);
    if (glGeom.lastProgram) {
      glGeom.hasSecondInstance = true;
    }
    glGeom.bySignature[program.id] = gps;
    glGeom.bySignature[signature] = gps;
    glGeom.lastGPS = gps;
    glGeom.lastProgram = program;
    glGeom.lastSignature = signature;
    gl.bindVertexArray(null);
    bufferSystem.unbind(constants.BUFFER_TYPE.ARRAY_BUFFER);
    return gps;
  }
  checkAttributes(geometry, program) {
    if (!geometry.attributeDirty) {
      return;
    }
    geometry.attributeDirty = false;
    const buffers = geometry.buffers;
    const attributes = geometry.attributes;
    const tempStride = {};
    const tempStart = {};
    for (const j in buffers) {
      tempStride[j] = 0;
      tempStart[j] = 0;
    }
    for (const j in attributes) {
      if (!attributes[j].size && program.attributeData[j]) {
        attributes[j].size = program.attributeData[j].size;
      } else if (!attributes[j].size) {
        console.warn(`PIXI Geometry attribute '${j}' size cannot be determined (likely the bound shader does not have the attribute)`);
      }
      const bufIndex = attributes[j].buffer;
      tempStride[bufIndex] += attributes[j].size * byteSizeMap[attributes[j].type];
      geometry.bufferStride[bufIndex] = tempStride[bufIndex];
    }
    for (const j in attributes) {
      const attribute = attributes[j];
      const attribSize = attribute.size;
      if (attribute.stride === void 0) {
        if (tempStride[attribute.buffer] === attribSize * byteSizeMap[attribute.type]) {
          attribute.stride = 0;
        } else {
          attribute.stride = tempStride[attribute.buffer];
        }
      }
      if (attribute.start === void 0) {
        attribute.start = tempStart[attribute.buffer];
        tempStart[attribute.buffer] += attribSize * byteSizeMap[attribute.type];
      }
    }
  }
  regenVao(geometry, glGeom) {
    if (glGeom.bufRefCount > 0) {
      return;
    }
    const { gl } = this.renderer;
    const gps = glGeom.lastGPS;
    gl.bindVertexArray(gps.vao);
    this.activateVao(geometry, glGeom.lastProgram);
    this.addRefBuffers(geometry, glGeom);
    if (!glGeom.hasSecondInstance) {
      return;
    }
    const old = glGeom.bySignature;
    glGeom.hasSecondInstance = false;
    glGeom.bySignature = {};
    for (const sig in old) {
      if (old[sig] === gps.vao) {
        glGeom.bySignature[sig] = new Geometry.GeometryPerShader(gps.vao);
        continue;
      }
      if (sig[0] === "g") {
        gl.deleteVertexArray(glGeom.bySignature[sig]);
      }
    }
  }
  addRefBuffers(geometry, glGeom) {
    if (glGeom.bufRefCount > 0) {
      return;
    }
    const bufferSystem = this.renderer?.buffer;
    const buffers = geometry.buffers;
    glGeom.bufRefCount++;
    for (let i = 0; i < buffers.length; i++) {
      const buffer = buffers[i];
      if (!buffer._glBuffers[glGeom.CONTEXT_UID]) {
        bufferSystem.bind(buffer);
      }
      buffer._glBuffers[glGeom.CONTEXT_UID].refCount++;
    }
  }
  /**
   * Disposes geometry.
   * @param geometry - Geometry with buffers. Only VAO will be disposed
   * @param [contextLost=false] - If context was lost, we suppress deleteVertexArray
   */
  disposeGeometry(geometry, contextLost) {
    if (!this.managedGeometries[geometry.id]) {
      return;
    }
    delete this.managedGeometries[geometry.id];
    const vaos = geometry.glVertexArrayObjects[this.CONTEXT_UID];
    const gl = this.gl;
    const buffers = geometry.buffers;
    const bufferSystem = this.renderer?.buffer;
    geometry.disposeRunner.remove(this);
    if (!vaos) {
      return;
    }
    if (bufferSystem) {
      for (let i = 0; i < buffers.length; i++) {
        const buf = buffers[i]._glBuffers[this.CONTEXT_UID];
        if (buf) {
          buf.refCount -= vaos.bufRefCount;
          if (buf.refCount === 0 && !contextLost) {
            bufferSystem.dispose(buffers[i], contextLost);
          }
        }
      }
    }
    if (!contextLost) {
      for (const vaoId in vaos) {
        if (vaoId[0] === "g") {
          const vao = vaos.bySignature[vaoId];
          if (this._activeGPS === vao) {
            this.unbind();
          }
          gl.deleteVertexArray(vao);
        }
      }
    }
    delete geometry.glVertexArrayObjects[this.CONTEXT_UID];
  }
  /**
   * swaps buffer to new one, saves contents
   * @param geometry
   * @param bufInd
   * @param newBuffer
   * @param copier
   */
  swapAndCopyBuffer(geometry, bufInd, newBuffer, copier = this.copier) {
    const oldBuffer = geometry.buffers[bufInd];
    const stride = geometry.getInstanceBufferStride(bufInd);
    this._activeBB = null;
    geometry.swapBuffer(bufInd, newBuffer);
    if (stride && oldBuffer.byteLength) {
      const bco = new BufferCopyOperation.BufferCopyOperation();
      bco.count = oldBuffer.byteLength / stride;
      copier.doCopy(this.renderer, oldBuffer, newBuffer, stride, [bco]);
    }
  }
  /**
   * Dispose all WebGL resources of all managed geometries.
   * @param [contextLost=false] - If context was lost, we suppress `gl.delete` calls
   */
  disposeAll(contextLost) {
    const all = Object.keys(this.managedGeometries);
    for (let i = 0; i < all.length; i++) {
      this.disposeGeometry(this.managedGeometries[all[i]], contextLost);
    }
  }
  /**
   * Activate vertex array object.
   * @param geometry - Geometry instance.
   * @param program - Shader program instance.
   */
  activateVao(geometry, program) {
    const gl = this.gl;
    const CONTEXT_UID = this.CONTEXT_UID;
    const bufferSystem = this.renderer.buffer;
    const buffers = geometry.buffers;
    const attributes = geometry.attributes;
    if (geometry.indexBuffer) {
      bufferSystem.bind(geometry.indexBuffer);
    }
    let lastBuffer = null;
    for (const j in attributes) {
      const attribute = attributes[j];
      const buffer = buffers[attribute.buffer];
      const glBuffer = buffer._glBuffers[CONTEXT_UID];
      if (program.attributeData[j]) {
        if (!glBuffer || lastBuffer !== glBuffer) {
          bufferSystem.bind(buffer);
          lastBuffer = buffer._glBuffers[CONTEXT_UID];
        }
        const location = program.attributeData[j].location;
        gl.enableVertexAttribArray(location);
        if (attribute.int) {
          gl.vertexAttribIPointer(location, attribute.size, attribute.type || gl.INT, attribute.stride, attribute.start);
        } else {
          gl.vertexAttribPointer(location, attribute.size, attribute.type || gl.FLOAT, attribute.normalized, attribute.stride, attribute.start);
        }
        if (attribute.instance) {
          if (this.hasInstance) {
            gl.vertexAttribDivisor(location, attribute.divisor);
          } else {
            throw new Error("geometry error, GPU Instancing is not supported on this device");
          }
        }
      }
    }
  }
  /**
   * Draws the currently bound geometry.
   * @param type - The type primitive to render.
   * @param size - The number of elements to be rendered. If not specified, all vertices after the
   *  starting vertex will be drawn.
   * @param start - The starting vertex in the geometry to start drawing from. If not specified,
   *  drawing will start from the first vertex.
   * @param instanceCount - The number of instances of the set of elements to execute. If not specified,
   *  all instances will be drawn.
   */
  draw(type, size, start, instanceCount) {
    const { gl } = this;
    const geometry = this._activeGeometry;
    if (this._activeGPS.emulateBaseInstance !== 0) {
      this.drawBI(type, size, start, instanceCount, 0);
    }
    if (geometry.indexBuffer) {
      const byteSize = geometry.indexBuffer.data.BYTES_PER_ELEMENT;
      const glType = byteSize === 2 ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT;
      if (byteSize === 2 || byteSize === 4 && this.canUseUInt32ElementIndex) {
        if (geometry.instanced) {
          gl.drawElementsInstanced(type, size || geometry.indexBuffer.data.length, glType, (start || 0) * byteSize, instanceCount || 1);
        } else {
          gl.drawElements(type, size || geometry.indexBuffer.data.length, glType, (start || 0) * byteSize);
        }
      } else {
        console.warn("unsupported index buffer type: uint32");
      }
    } else if (geometry.instanced) {
      gl.drawArraysInstanced(type, start, size || geometry.getSize(), instanceCount || 1);
    } else {
      gl.drawArrays(type, start, size || geometry.getSize());
    }
    return this;
  }
  drawBI(type, size, start, instanceCount, baseInstance = 0) {
    const { renderer } = this;
    const { gl } = renderer;
    const { bvbi } = renderer.context.extensions;
    const geometry = this._activeGeometry;
    const gps = this._activeGPS;
    const program = this.renderer.shader.shader.program;
    if (bvbi) {
      bvbi.drawArraysInstancedBaseInstanceWEBGL(type, start, size, instanceCount, baseInstance);
    } else {
      const attribSync = geometry.getAttributeBaseCallback();
      if (!gps.instLocations) {
        gps.instLocations = program.getLocationListByAttributes(geometry.getInstancedAttributeNames());
      }
      if (attribSync.bufSyncCount === 0) {
        const bb = attribSync.bufFirstIndex;
        if (this._activeBB !== geometry.buffers[bb]) {
          this._activeBB = geometry.buffers[bb];
          renderer.buffer.bind(this._activeBB);
        }
        if (gps.emulateBaseInstance !== baseInstance) {
          gps.emulateBaseInstance = baseInstance;
          attribSync.syncFunc(gl, gps.instLocations, baseInstance);
        }
        gl.drawArraysInstanced(type, start, size, instanceCount);
      } else {
        const buffers = geometry.buffers;
        const bufferSystem = renderer.buffer;
        if (gps.emulateBaseInstance !== baseInstance) {
          gps.emulateBaseInstance = baseInstance;
          this._activeBB = attribSync.syncFunc(gl, gps.instLocations, baseInstance, bufferSystem, buffers, this._activeBB);
        }
        gl.drawArraysInstanced(type, start, size, instanceCount);
      }
    }
  }
  multiDrawArraysBVBI(type, firsts, counts, instanceCounts, instanceOffsets, rangeCount) {
    const { renderer } = this;
    const { gl } = renderer;
    const geometry = this._activeGeometry;
    const gps = this._activeGPS;
    const program = this.renderer.shader.shader.program;
    const { md_bvbi } = renderer.context.extensions;
    if (md_bvbi) {
      md_bvbi.multiDrawArraysInstancedBaseInstanceWEBGL(type, firsts, 0, counts, 0, instanceCounts, 0, instanceOffsets, 0, rangeCount);
    } else {
      const attribSync = geometry.getAttributeBaseCallback();
      if (!gps.instLocations) {
        gps.instLocations = program.getLocationListByAttributes(geometry.getInstancedAttributeNames());
      }
      if (attribSync.bufSyncCount === 0) {
        renderer.buffer.bind(geometry.buffers[attribSync.bufFirstIndex]);
        for (let i = 0; i < rangeCount; i++) {
          if (gps.emulateBaseInstance !== instanceOffsets[i]) {
            gps.emulateBaseInstance = instanceOffsets[i];
            attribSync.syncFunc(gl, gps.instLocations, instanceOffsets[i]);
          }
          gl.drawArraysInstanced(type, 0, counts[i], instanceCounts[i]);
        }
      } else {
        const buffers = geometry.buffers;
        const bufferSystem = renderer.buffer;
        for (let i = 0; i < rangeCount; i++) {
          if (gps.emulateBaseInstance !== instanceOffsets[i]) {
            gps.emulateBaseInstance = instanceOffsets[i];
            this._activeBB = attribSync.syncFunc(gl, gps.instLocations, instanceOffsets[i], bufferSystem, buffers, this._activeBB);
          }
          gl.drawArraysInstanced(type, 0, counts[i], instanceCounts[i]);
        }
      }
    }
  }
  /** Unbind/reset everything. */
  unbind() {
    this.gl.bindVertexArray(null);
    this._activeGPS = null;
    this._activeGeometry = null;
    this._activeBB = null;
  }
  destroy() {
    this.renderer = null;
  }
}
GeometrySystem.extension = {
  type: extensions.ExtensionType.RendererSystem,
  name: "geometry"
};
extensions.extensions.add(GeometrySystem);

exports.GeometrySystem = GeometrySystem;
//# sourceMappingURL=GeometrySystem.js.map
