import { T as TRIANGLE_STRIP, Y as QUAD_STRIP, X as TRIANGLE_FAN, c as TRIANGLES } from '../constants-DFRExFht.js';
import '../math/p5.Matrix.js';
import { Geometry } from './p5.Geometry.js';
import { Matrix } from '../math/Matrices/Matrix.js';
import '../math/p5.Vector.js';
import '../math/Matrices/MatrixInterface.js';
import './p5.DataArray.js';
import '../io/utilities.js';

/**
 * @private
 * A class responsible for converting successive WebGL draw calls into a single
 * `p5.Geometry` that can be reused and drawn with `model()`.
 */
class GeometryBuilder {
  constructor(renderer) {
    this.renderer = renderer;
    renderer._pInst.push();
    this.identityMatrix = new Matrix(4);
    renderer.states.setValue('uModelMatrix', new Matrix(4));
    this.geometry = new Geometry(
      undefined,
      undefined,
      undefined,
      this.renderer
    );
    this.geometry.gid = `_p5_GeometryBuilder_${GeometryBuilder.nextGeometryId}`;
    GeometryBuilder.nextGeometryId++;
    this.hasTransform = false;
  }

  /**
   * @private
   * Applies the current transformation matrix to each vertex.
   */
  transformVertices(vertices) {
    if (!this.hasTransform) return vertices;

    return vertices.map(v =>
      this.renderer.states.uModelMatrix.multiplyPoint(v)
    );
  }

  /**
   * @private
   * Applies the current normal matrix to each normal.
   */
  transformNormals(normals) {
    if (!this.hasTransform) return normals;

    return normals.map(
      v => this.renderer.scratchMat3.multiplyVec(v) // this is a vec3
    );
  }

  /**
   * @private
   * Adds a p5.Geometry to the builder's combined geometry, flattening
   * transformations.
   */
  addGeometry(input) {
    this.hasTransform = !this.renderer.states.uModelMatrix.mat4
      .every((v, i) => v === this.identityMatrix.mat4[i]);

    if (this.hasTransform) {
      this.renderer.scratchMat3.inverseTranspose4x4(
        this.renderer.states.uModelMatrix
      );
    }

    let startIdx = this.geometry.vertices.length;
    for (const v of this.transformVertices(input.vertices)) {
      this.geometry.vertices.push(v);
    }
    for (const vn of this.transformNormals(input.vertexNormals)) {
      this.geometry.vertexNormals.push(vn);
    }
    for (const val of input.uvs) {
      this.geometry.uvs.push(val);
    }

    const inputUserVertexProps = input.userVertexProperties;
    const builtUserVertexProps = this.geometry.userVertexProperties;
    const numPreviousVertices =
      this.geometry.vertices.length - input.vertices.length;

    for (const propName in builtUserVertexProps){
      if (propName in inputUserVertexProps){
        continue;
      }
      const prop = builtUserVertexProps[propName];
      const size = prop.getDataSize();
      const numMissingValues = size * input.vertices.length;
      const missingValues = Array(numMissingValues).fill(0);
      prop.pushDirect(missingValues);
    }
    for (const propName in inputUserVertexProps){
      const prop = inputUserVertexProps[propName];
      const data = prop.getSrcArray();
      const size = prop.getDataSize();
      if (numPreviousVertices > 0 && !(propName in builtUserVertexProps)){
        const numMissingValues = size * numPreviousVertices;
        const missingValues = Array(numMissingValues).fill(0);
        this.geometry.vertexProperty(propName, missingValues, size);
      }
      this.geometry.vertexProperty(propName, data, size);
    }

    if (this.renderer.states.fillColor) {
      this.geometry.faces.push(
        ...input.faces.map(f => f.map(idx => idx + startIdx))
      );
    }
    if (this.renderer.states.strokeColor) {
      for (const edge of input.edges.map(edge => edge.map(idx => idx + startIdx))) {
        this.geometry.edges.push(edge);
      }
    }
    const vertexColors = [...input.vertexColors];
    while (vertexColors.length < input.vertices.length * 4) {
      vertexColors.push(...this.renderer.states.curFillColor);
    }
    for (const c of vertexColors) {
      this.geometry.vertexColors.push(c);
    }
  }

  /**
   * Adds geometry from the renderer's immediate mode into the builder's
   * combined geometry.
   */
  addImmediate(geometry, shapeMode, { validateFaces = false } = {}) {
    const faces = [];

    if (this.renderer.states.fillColor) {
      if (
        shapeMode === TRIANGLE_STRIP ||
        shapeMode === QUAD_STRIP
      ) {
        for (let i = 2; i < geometry.vertices.length; i++) {
          if (i % 2 === 0) {
            faces.push([i, i - 1, i - 2]);
          } else {
            faces.push([i, i - 2, i - 1]);
          }
        }
      } else if (shapeMode === TRIANGLE_FAN) {
        for (let i = 2; i < geometry.vertices.length; i++) {
          faces.push([0, i - 1, i]);
        }
      } else if (shapeMode === TRIANGLES) {
        for (let i = 0; i < geometry.vertices.length; i += 3) {
          if (
            !validateFaces ||
            geometry.vertices[i].copy().sub(geometry.vertices[i+1])
              .cross(geometry.vertices[i].copy().sub(geometry.vertices[i+2]))
              .magSq() > 0
          ) {
            faces.push([i, i + 1, i + 2]);
          }
        }
      }
    }
    this.addGeometry(Object.assign({}, geometry, { faces }));
  }

  /**
   * Adds geometry from the renderer's retained mode into the builder's
   * combined geometry.
   */
  addRetained(geometry) {
    this.addGeometry(geometry);
  }

  /**
   * Cleans up the state of the renderer and returns the combined geometry that
   * was built.
   * @returns p5.Geometry The flattened, combined geometry
   */
  finish() {
    this.renderer._pInst.pop();
    return this.geometry;
  }
}

/**
 * Keeps track of how many custom geometry objects have been made so that each
 * can be assigned a unique ID.
 */
GeometryBuilder.nextGeometryId = 0;

export { GeometryBuilder as default };
