import buildDrawCommand from "./buildDrawCommand.js";
import BoundingSphere from "../../Core/BoundingSphere.js";
import Check from "../../Core/Check.js";
import defaultValue from "../../Core/defaultValue.js";
import defined from "../../Core/defined.js";
import Matrix4 from "../../Core/Matrix4.js";
import ModelExperimentalPrimitive from "./ModelExperimentalPrimitive.js";
import ModelExperimentalNode from "./ModelExperimentalNode.js";
import ModelExperimentalUtility from "./ModelExperimentalUtility.js";
import ModelRenderResources from "./ModelRenderResources.js";
import NodeRenderResources from "./NodeRenderResources.js";
import PrimitiveRenderResources from "./PrimitiveRenderResources.js";

/**
 * An in memory representation of the scene graph for a {@link ModelExperimental}
 *
 * @param {Object} options An object containing the following options
 * @param {ModelExperimental} options.model The model this scene graph belongs to
 * @param {ModelComponents} options.modelComponents The model components describing the model
 *
 * @alias ModelExperimentalSceneGraph
 * @constructor
 *
 * @private
 */
export default function ModelExperimentalSceneGraph(options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("options.model", options.model);
  Check.typeOf.object("options.modelComponents", options.modelComponents);
  //>>includeEnd('debug');

  /**
   * A reference to the {@link ModelExperimental} that owns this scene graph.
   *
   * @type {ModelExperimental}
   * @readonly
   *
   * @private
   */
  this._model = options.model;

  /**
   * The model components that represent the contents of the 3D model file.
   *
   * @type {ModelComponents}
   * @readonly
   *
   * @private
   */
  this._modelComponents = options.modelComponents;

  /**
   * Pipeline stages to apply across the model.
   *
   * @type {Object[]}
   * @readonly
   *
   * @private
   */
  this._pipelineStages = [];

  /**
   * The runtime nodes that make up the scene graph
   *
   * @type {ModelExperimentalNode[]}
   * @readonly
   *
   * @private
   */
  this._runtimeNodes = [];

  /**
   * Once computed, the {@link DrawCommand}s that are used to render this
   * scene graph are stored here.
   *
   * @type {DrawCommand[]}
   * @readonly
   *
   * @private
   */
  this._drawCommands = [];

  /**
   * The bounding sphere containing all the primitives in the scene graph.
   *
   * @type {BoundingSphere}
   * @readonly
   *
   * @private
   */
  this._boundingSphere = undefined;

  /**
   * The array of bounding spheres of all the primitives in the scene graph.
   *
   * @type {BoundingSphere[]}
   * @readonly
   *
   * @private
   */
  this._boundingSpheres = [];

  initialize(this);
}

function initialize(sceneGraph) {
  var modelMatrix = Matrix4.clone(sceneGraph._model.modelMatrix);
  var scene = sceneGraph._modelComponents.scene;

  ModelExperimentalUtility.correctModelMatrix(
    modelMatrix,
    scene.upAxis,
    scene.forwardAxis
  );

  var rootNodes = sceneGraph._modelComponents.scene.nodes;
  for (var i = 0; i < rootNodes.length; i++) {
    var rootNode = sceneGraph._modelComponents.scene.nodes[i];
    var rootNodeModelMatrix = Matrix4.multiply(
      modelMatrix,
      ModelExperimentalUtility.getNodeTransform(rootNode),
      new Matrix4()
    );

    traverseSceneGraph(sceneGraph, rootNode, rootNodeModelMatrix);
  }
}

/**
 * Recursively traverse through the nodes in the scene graph, using depth-first
 * post-order traversal.
 *
 * @param {ModelSceneGraph} sceneGraph The scene graph
 * @param {ModelComponents.Node} node The current node
 * @param {Matrix4} modelMatrix The current computed model matrix for this node.
 *
 * @private
 */
function traverseSceneGraph(sceneGraph, node, modelMatrix) {
  // No processing needs to happen if node has no children and no mesh primitives.
  if (!defined(node.children) && !defined(node.primitives)) {
    return;
  }

  // Traverse through scene graph.
  var i;
  if (defined(node.children)) {
    for (i = 0; i < node.children.length; i++) {
      var childNode = node.children[i];
      var childNodeModelMatrix = Matrix4.multiply(
        modelMatrix,
        ModelExperimentalUtility.getNodeTransform(childNode),
        new Matrix4()
      );

      traverseSceneGraph(sceneGraph, childNode, childNodeModelMatrix);
    }
  }

  // Process node and mesh primitives.
  var runtimeNode = new ModelExperimentalNode({
    node: node,
    modelMatrix: modelMatrix,
  });

  if (defined(node.primitives)) {
    for (i = 0; i < node.primitives.length; i++) {
      runtimeNode.runtimePrimitives.push(
        new ModelExperimentalPrimitive({
          primitive: node.primitives[i],
          model: sceneGraph._model,
        })
      );
    }
  }

  sceneGraph._runtimeNodes.push(runtimeNode);
}

/**
 * Generates the draw commands for each primitive in the model.
 *
 * @param {FrameState} frameState The current frame state. This is needed to
 * allocate GPU resources as needed.
 *
 * @private
 */
ModelExperimentalSceneGraph.prototype.buildDrawCommands = function (
  frameState
) {
  var modelRenderResources = new ModelRenderResources(this._model);

  var i, j, k;
  for (i = 0; i < this._runtimeNodes.length; i++) {
    var runtimeNode = this._runtimeNodes[i];

    var nodeRenderResources = new NodeRenderResources(
      modelRenderResources,
      runtimeNode
    );

    for (j = 0; j < runtimeNode.pipelineStages.length; j++) {
      var nodePipelineStage = runtimeNode.pipelineStages[j];

      nodePipelineStage.process(
        nodeRenderResources,
        runtimeNode.node,
        frameState
      );
    }

    for (j = 0; j < runtimeNode.runtimePrimitives.length; j++) {
      var runtimePrimitive = runtimeNode.runtimePrimitives[j];

      var primitiveRenderResources = new PrimitiveRenderResources(
        nodeRenderResources,
        runtimePrimitive
      );

      for (k = 0; k < runtimePrimitive.pipelineStages.length; k++) {
        var primitivePipelineStage = runtimePrimitive.pipelineStages[k];

        primitivePipelineStage.process(
          primitiveRenderResources,
          runtimePrimitive.primitive,
          frameState
        );
      }

      this._boundingSpheres.push(primitiveRenderResources.boundingSphere);

      var drawCommand = buildDrawCommand(primitiveRenderResources, frameState);
      this._drawCommands.push(drawCommand);
    }
  }
  this._boundingSphere = BoundingSphere.fromBoundingSpheres(
    this._boundingSpheres
  );
};
