import Check from "../Core/Check.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defined from "../Core/defined.js";
import IndexDatatype from "../Core/IndexDatatype.js";
import Buffer from "../Renderer/Buffer.js";
import BufferUsage from "../Renderer/BufferUsage.js";
import AttributeType from "./AttributeType.js";
import ModelComponents from "./ModelComponents.js";
import PrimitiveOutlineGenerator from "./Model/PrimitiveOutlineGenerator.js";

/**
 * Simple struct for tracking whether an attribute will be loaded as a buffer
 * or typed array after post-processing.
 *
 * @alias PrimitiveLoadPlan.AttributeLoadPlan
 * @constructor
 *
 * @param {ModelComponents.Attribute} attribute The attribute to be updated
 *
 * @private
 */
function AttributeLoadPlan(attribute) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("attribute", attribute);
  //>>includeEnd('debug');

  /**
   * The attribute to track.
   *
   * @type {ModelComponents.Attribute}
   * @readonly
   * @private
   */
  this.attribute = attribute;

  /**
   * Whether the attribute will be loaded as a GPU buffer by the time
   * {@link PrimitiveLoadPlan#postProcess} is finished.
   *
   * @type {Boolean}
   * @private
   */
  this.loadBuffer = false;

  /**
   * Whether the attribute will be loaded as a packed typed array by the time
   * {@link PrimitiveLoadPlan#postProcess} is finished.
   *
   * @type {Boolean}
   * @private
   */
  this.loadTypedArray = false;
}

/**
 * Simple struct for tracking whether an index buffer will be loaded as a buffer
 * or typed array after post-processing.
 *
 * @alias PrimitiveLoadPlan.IndicesLoadPlan
 * @constructor
 *
 * @param {ModelComponents.Indices} indices The indices to be updated
 *
 * @private
 */
function IndicesLoadPlan(indices) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("indices", indices);
  //>>includeEnd('debug');

  /**
   * The indices to track.
   *
   * @type {ModelComponents.Indices}
   * @readonly
   * @private
   */
  this.indices = indices;

  /**
   * Whether the indices will be loaded as a GPU buffer by the time
   * {@link PrimitiveLoadPlan#postProcess} is finished.
   *
   * @type {Boolean}
   * @private
   */
  this.loadBuffer = false;

  /**
   * Whether the indices will be loaded as a typed array copy of the GPU
   * buffer by the time {@link PrimitiveLoadPlan#postProcess} is finished.
   *
   * @type {Boolean}
   * @private
   */
  this.loadTypedArray = false;
}

/**
 * Primitives may need post-processing steps after their attributes and indices
 * have loaded, such as generating outlines for the CESIUM_primitive_outline glTF
 * extension. This object tracks what indices and attributes need to be
 * post-processed.
 *
 * @alias PrimitiveLoadPlan
 * @constructor
 *
 * @param {ModelComponents.Primitive} primitive The primitive to track
 *
 * @private
 */
function PrimitiveLoadPlan(primitive) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("primitive", primitive);
  //>>includeEnd('debug');

  /**
   * The primitive to track.
   *
   * @type {ModelComponents.Primitive}
   * @readonly
   * @private
   */
  this.primitive = primitive;

  /**
   * A flat list of attributes that need to be post-processed. This includes
   * both regular attributes and morph target attributes.
   *
   * @type {PrimitiveLoadPlan.AttributeLoadPlan[]}
   * @private
   */
  this.attributePlans = [];

  /**
   * Information about the triangle indices that need to be post-processed,
   * if they exist.
   *
   * @type {PrimitiveLoadPlan.IndicesLoadPlan}
   * @private
   */
  this.indicesPlan = undefined;

  /**
   * Set this true to indicate that the primitive has the
   * CESIUM_primitive_outline extension and needs to be post-processed
   *
   * @type {Boolean}
   * @private
   */
  this.needsOutlines = false;

  /**
   * The outline edge indices from the CESIUM_primitive_outline extension
   *
   * @type {Number[]}
   * @private
   */
  this.outlineIndices = undefined;
}

/**
 * Apply post-processing steps that may modify geometry such as generating
 * outline coordinates. If no post-processing steps are needed, this function
 * is a no-op.
 *
 * @param {Context} context The context for generating buffers on the GPU
 */
PrimitiveLoadPlan.prototype.postProcess = function (context) {
  // Handle CESIUM_primitive_outline. This modifies indices and attributes and
  // also generates a new attribute for the outline coordinates. These steps
  // are synchronous.
  if (this.needsOutlines) {
    generateOutlines(this);
    generateBuffers(this, context);
  }
};

function generateOutlines(loadPlan) {
  const primitive = loadPlan.primitive;
  const indices = primitive.indices;

  const vertexCount = primitive.attributes[0].count;

  const generator = new PrimitiveOutlineGenerator({
    triangleIndices: indices.typedArray,
    outlineIndices: loadPlan.outlineIndices,
    originalVertexCount: vertexCount,
  });

  // The generator modifies/adds indices. In some uncommon cases it may have
  // to upgrade to 16- or 32-bit indices so the datatype may change.
  indices.typedArray = generator.updatedTriangleIndices;
  indices.indexDatatype = IndexDatatype.fromTypedArray(indices.typedArray);

  // The outline generator creates a new attribute for the outline coordinates
  // that are used with a lookup texture.
  const outlineCoordinates = makeOutlineCoordinatesAttribute(
    generator.outlineCoordinates
  );
  const outlineCoordinatesPlan = new AttributeLoadPlan(outlineCoordinates);
  outlineCoordinatesPlan.loadBuffer = true;
  outlineCoordinatesPlan.loadTypedArray = false;
  loadPlan.attributePlans.push(outlineCoordinatesPlan);
  primitive.outlineCoordinates = outlineCoordinatesPlan.attribute;

  // Some vertices may be copied due to the addition of the new attribute
  // which may have multiple values at a vertex depending on the face
  const attributePlans = loadPlan.attributePlans;
  const attributesLength = loadPlan.attributePlans.length;
  for (let i = 0; i < attributesLength; i++) {
    const attribute = attributePlans[i].attribute;
    attribute.typedArray = generator.updateAttribute(attribute.typedArray);
  }
}

function makeOutlineCoordinatesAttribute(outlineCoordinatesTypedArray) {
  const attribute = new ModelComponents.Attribute();
  attribute.name = "_OUTLINE_COORDINATES";
  attribute.typedArray = outlineCoordinatesTypedArray;
  attribute.componentDatatype = ComponentDatatype.FLOAT;
  attribute.type = AttributeType.VEC3;
  attribute.normalized = false;
  attribute.count = outlineCoordinatesTypedArray.length / 3;

  return attribute;
}

function generateBuffers(loadPlan, context) {
  generateAttributeBuffers(loadPlan.attributePlans, context);

  if (defined(loadPlan.indicesPlan)) {
    generateIndexBuffers(loadPlan.indicesPlan, context);
  }
}

function generateAttributeBuffers(attributePlans, context) {
  const attributesLength = attributePlans.length;
  for (let i = 0; i < attributesLength; i++) {
    const attributePlan = attributePlans[i];
    const attribute = attributePlan.attribute;
    const typedArray = attribute.typedArray;

    if (attributePlan.loadBuffer) {
      const buffer = Buffer.createVertexBuffer({
        typedArray: typedArray,
        context: context,
        usage: BufferUsage.STATIC_DRAW,
      });
      buffer.vertexArrayDestroyable = false;
      attribute.buffer = buffer;
    }

    if (!attributePlan.loadTypedArray) {
      attribute.typedArray = undefined;
    }
  }
}

function generateIndexBuffers(indicesPlan, context) {
  const indices = indicesPlan.indices;
  if (indicesPlan.loadBuffer) {
    const buffer = Buffer.createIndexBuffer({
      typedArray: indices.typedArray,
      context: context,
      usage: BufferUsage.STATIC_DRAW,
      indexDatatype: indices.indexDatatype,
    });
    indices.buffer = buffer;
    buffer.vertexArrayDestroyable = false;
  }

  if (!indicesPlan.loadTypedArray) {
    indices.typedArray = undefined;
  }
}

PrimitiveLoadPlan.AttributeLoadPlan = AttributeLoadPlan;
PrimitiveLoadPlan.IndicesLoadPlan = IndicesLoadPlan;
export default PrimitiveLoadPlan;
