import Check from "../../Core/Check.js";
import defaultValue from "../../Core/defaultValue.js";
import defined from "../../Core/defined.js";
import PrimitiveType from "../../Core/PrimitiveType.js";
import SceneMode from "../SceneMode.js";
import AlphaPipelineStage from "./AlphaPipelineStage.js";
import BatchTexturePipelineStage from "./BatchTexturePipelineStage.js";
import ClassificationPipelineStage from "./ClassificationPipelineStage.js";
import CPUStylingPipelineStage from "./CPUStylingPipelineStage.js";
import CustomShaderMode from "./CustomShaderMode.js";
import CustomShaderPipelineStage from "./CustomShaderPipelineStage.js";
import DequantizationPipelineStage from "./DequantizationPipelineStage.js";
import FeatureIdPipelineStage from "./FeatureIdPipelineStage.js";
import GeometryPipelineStage from "./GeometryPipelineStage.js";
import LightingPipelineStage from "./LightingPipelineStage.js";
import MaterialPipelineStage from "./MaterialPipelineStage.js";
import MetadataPipelineStage from "./MetadataPipelineStage.js";
import ModelUtility from "./ModelUtility.js";
import MorphTargetsPipelineStage from "./MorphTargetsPipelineStage.js";
import PickingPipelineStage from "./PickingPipelineStage.js";
import PointCloudStylingPipelineStage from "./PointCloudStylingPipelineStage.js";
import PrimitiveOutlinePipelineStage from "./PrimitiveOutlinePipelineStage.js";
import PrimitiveStatisticsPipelineStage from "./PrimitiveStatisticsPipelineStage.js";
import SceneMode2DPipelineStage from "./SceneMode2DPipelineStage.js";
import SelectedFeatureIdPipelineStage from "./SelectedFeatureIdPipelineStage.js";
import SkinningPipelineStage from "./SkinningPipelineStage.js";
import WireframePipelineStage from "./WireframePipelineStage.js";

/**
 * In memory representation of a single primitive, that is, a primitive
 * and its corresponding mesh.
 *
 * @param {Object} options An object containing the following options:
 * @param {ModelComponents.Primitive} options.primitive The primitive component.
 * @param {ModelComponents.Node} options.node The node that this primitive belongs to.
 * @param {Model} options.model The {@link Model} this primitive belongs to.
 *
 * @alias ModelRuntimePrimitive
 * @constructor
 *
 * @private
 */
function ModelRuntimePrimitive(options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  const primitive = options.primitive;
  const node = options.node;
  const model = options.model;
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("options.primitive", primitive);
  Check.typeOf.object("options.node", node);
  Check.typeOf.object("options.model", model);
  //>>includeEnd('debug');

  /**
   * The primitive component associated with this primitive.
   *
   * @type {ModelComponents.Primitive}
   *
   * @private
   */
  this.primitive = primitive;

  /**
   * A reference to the node this primitive belongs to.
   *
   * @type {ModelComponents.Node}
   *
   * @private
   */
  this.node = node;

  /**
   * A reference to the model
   *
   * @type {Model}
   *
   * @private
   */
  this.model = model;

  /**
   * Pipeline stages to apply to this primitive. This
   * is an array of classes, each with a static method called
   * <code>process()</code>
   *
   * @type {Object[]}
   * @readonly
   *
   * @private
   */
  this.pipelineStages = [];

  /**
   * The generated {@link ModelDrawCommand} or {@link ClassificationModelDrawCommand}
   * associated with this primitive.
   *
   * @type {ModelDrawCommand|ClassificationModelDrawCommand}
   *
   * @private
   */
  this.drawCommand = undefined;

  /**
   * The bounding sphere of this primitive in object-space.
   *
   * @type {BoundingSphere}
   *
   * @private
   */
  this.boundingSphere = undefined;

  /**
   * The bounding sphere of this primitive in 2D world space.
   *
   * @type {BoundingSphere}
   *
   * @private
   */
  this.boundingSphere2D = undefined;

  /**
   * A buffer containing the primitive's positions projected to 2D world
   * coordinates. This is generated by SceneMode2DPipelineStage and used for
   * rendering in 2D / CV mode. The memory is managed by Model; this is just
   * a reference.
   *
   * @type {Buffer}
   *
   * @private
   */
  this.positionBuffer2D = undefined;

  /**
   * An array containing the lengths of the vertex batches for classification.
   * Vertices with the same feature ID are batched together, and each batch is
   * drawn with a different draw command in order to properly classify other
   * assets.
   * <p>
   * This is generated by ClassificationPipelineStage. The memory is managed by
   * Model; this is just a reference.
   * </p>
   *
   * @type {Number[]}
   *
   * @private
   */
  this.batchLengths = undefined;

  /**
   * An array containing the offsets of the vertex batches for classification.
   * Vertices with the same feature ID are batched together, and each batch is
   * drawn with a different draw command in order to properly classify other
   * assets.
   * <p>
   * This is generated by ClassificationPipelineStage. The memory is managed by
   * Model; this is just a reference.
   * </p>
   *
   * @type {Number[]}
   *
   * @private
   */
  this.batchOffsets = undefined;

  /**
   * Update stages to apply to this primitive.
   *
   * @type {Object[]}
   * @readonly
   *
   * @private
   */
  this.updateStages = [];
}

/**
 * Configure the primitive pipeline stages. If the pipeline needs to be re-run,
 * call this method again to ensure the correct sequence of pipeline stages are
 * used.
 *
 * @param {FrameState} frameState The frame state.
 *
 * @private
 */
ModelRuntimePrimitive.prototype.configurePipeline = function (frameState) {
  const pipelineStages = this.pipelineStages;
  pipelineStages.length = 0;

  const primitive = this.primitive;
  const node = this.node;
  const model = this.model;

  const customShader = model.customShader;
  const style = model.style;

  const useWebgl2 = frameState.context.webgl2;
  const mode = frameState.mode;
  const use2D =
    mode !== SceneMode.SCENE3D && !frameState.scene3DOnly && model._projectTo2D;

  const hasMorphTargets =
    defined(primitive.morphTargets) && primitive.morphTargets.length > 0;
  const hasSkinning = defined(node.skin);
  const hasCustomShader = defined(customShader);
  const hasCustomFragmentShader =
    hasCustomShader && defined(customShader.fragmentShaderText);
  const materialsEnabled =
    !hasCustomFragmentShader ||
    customShader.mode !== CustomShaderMode.REPLACE_MATERIAL;
  const hasQuantization = ModelUtility.hasQuantizedAttributes(
    primitive.attributes
  );
  const generateWireframeIndices =
    model.debugWireframe &&
    PrimitiveType.isTriangles(primitive.primitiveType) &&
    // Generating index buffers for wireframes is always possible in WebGL2.
    // However, this will only work in WebGL1 if the model was constructed with
    // enableDebugWireframe set to true.
    (model._enableDebugWireframe || useWebgl2);

  const pointCloudShading = model.pointCloudShading;
  const hasAttenuation =
    defined(pointCloudShading) && pointCloudShading.attenuation;
  const hasPointCloudBackFaceCulling =
    defined(pointCloudShading) && pointCloudShading.backFaceCulling;
  const hasPointCloudStyle =
    primitive.primitiveType === PrimitiveType.POINTS &&
    (defined(style) || hasAttenuation || hasPointCloudBackFaceCulling);

  const hasOutlines =
    model._enableShowOutline && defined(primitive.outlineCoordinates);

  const featureIdFlags = inspectFeatureIds(model, node, primitive);

  const hasClassification = defined(model.classificationType);

  // Start of pipeline -----------------------------------------------------
  if (use2D) {
    pipelineStages.push(SceneMode2DPipelineStage);
  }

  pipelineStages.push(GeometryPipelineStage);

  if (generateWireframeIndices) {
    pipelineStages.push(WireframePipelineStage);
  }

  if (hasClassification) {
    pipelineStages.push(ClassificationPipelineStage);
  }

  if (hasMorphTargets) {
    pipelineStages.push(MorphTargetsPipelineStage);
  }

  if (hasSkinning) {
    pipelineStages.push(SkinningPipelineStage);
  }

  if (hasPointCloudStyle) {
    pipelineStages.push(PointCloudStylingPipelineStage);
  }

  if (hasQuantization) {
    pipelineStages.push(DequantizationPipelineStage);
  }

  if (materialsEnabled) {
    pipelineStages.push(MaterialPipelineStage);
  }

  // These stages are always run to ensure structs
  // are declared to avoid compilation errors.
  pipelineStages.push(FeatureIdPipelineStage);
  pipelineStages.push(MetadataPipelineStage);

  if (featureIdFlags.hasPropertyTable) {
    pipelineStages.push(SelectedFeatureIdPipelineStage);
    pipelineStages.push(BatchTexturePipelineStage);
    pipelineStages.push(CPUStylingPipelineStage);
  }

  if (hasCustomShader) {
    pipelineStages.push(CustomShaderPipelineStage);
  }

  pipelineStages.push(LightingPipelineStage);

  if (model.allowPicking) {
    pipelineStages.push(PickingPipelineStage);
  }

  if (hasOutlines) {
    pipelineStages.push(PrimitiveOutlinePipelineStage);
  }

  pipelineStages.push(AlphaPipelineStage);

  pipelineStages.push(PrimitiveStatisticsPipelineStage);

  return;
};

function inspectFeatureIds(model, node, primitive) {
  let featureIds;
  // Check instances first, as this is the most specific type of
  // feature ID
  if (defined(node.instances)) {
    featureIds = ModelUtility.getFeatureIdsByLabel(
      node.instances.featureIds,
      model.instanceFeatureIdLabel
    );

    if (defined(featureIds)) {
      return {
        hasFeatureIds: true,
        hasPropertyTable: defined(featureIds.propertyTableId),
      };
    }
  }

  featureIds = ModelUtility.getFeatureIdsByLabel(
    primitive.featureIds,
    model.featureIdLabel
  );
  if (defined(featureIds)) {
    return {
      hasFeatureIds: true,
      hasPropertyTable: defined(featureIds.propertyTableId),
    };
  }

  return {
    hasFeatureIds: false,
    hasPropertyTable: false,
  };
}

export default ModelRuntimePrimitive;
