import BoundingSphere from "../../Core/BoundingSphere.js";
import Cartesian2 from "../../Core/Cartesian2.js";
import CesiumMath from "../../Core/Math.js";
import Check from "../../Core/Check.js";
import clone from "../../Core/clone.js";
import defaultValue from "../../Core/defaultValue.js";
import defined from "../../Core/defined.js";
import Matrix4 from "../../Core/Matrix4.js";
import WebGLConstants from "../../Core/WebGLConstants.js";
import DrawCommand from "../../Renderer/DrawCommand.js";
import Pass from "../../Renderer/Pass.js";
import RenderState from "../../Renderer/RenderState.js";
import BlendingState from "../BlendingState.js";
import CullFace from "../CullFace.js";
import SceneMode from "../SceneMode.js";
import ShadowMode from "../ShadowMode.js";
import StencilConstants from "../StencilConstants.js";
import StencilFunction from "../StencilFunction.js";
import StencilOperation from "../StencilOperation.js";
import StyleCommandsNeeded from "./StyleCommandsNeeded.js";

/**
 * A wrapper around the draw commands used to render a {@link ModelRuntimePrimitive}.
 * This manages the derived commands and pushes only the necessary commands depending
 * on the given frame state.
 *
 * @param {object} options An object containing the following options:
 * @param {DrawCommand} options.command The draw command from which to derive other commands from.
 * @param {PrimitiveRenderResources} options.primitiveRenderResources The render resources of the primitive associated with the command.
 *
 * @alias ModelDrawCommand
 * @constructor
 *
 * @private
 */
function ModelDrawCommand(options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  const command = options.command;
  const renderResources = options.primitiveRenderResources;

  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("options.command", command);
  Check.typeOf.object("options.primitiveRenderResources", renderResources);
  //>>includeEnd('debug');

  const model = renderResources.model;
  this._model = model;

  const runtimePrimitive = renderResources.runtimePrimitive;
  this._runtimePrimitive = runtimePrimitive;

  // If the command is translucent, or if the primitive's material is
  // double-sided, then back-face culling is automatically disabled for
  // the command. The user value for back-face culling will be ignored.
  const isTranslucent = command.pass === Pass.TRANSLUCENT;
  const isDoubleSided = runtimePrimitive.primitive.material.doubleSided;
  const usesBackFaceCulling = !isDoubleSided && !isTranslucent;
  const hasSilhouette = renderResources.hasSilhouette;

  // If the command was already translucent, there's no need to derive a new
  // translucent command. As of now, a style can't change an originally
  // translucent feature to opaque since the style's alpha is modulated,
  // not replaced. When this changes, we need to derive new opaque commands
  // in initialize().
  //
  // Silhouettes for primitives with both opaque and translucent features
  // are not yet supported.
  const needsTranslucentCommand = !isTranslucent && !hasSilhouette;

  const needsSkipLevelOfDetailCommands =
    renderResources.hasSkipLevelOfDetail && !isTranslucent;

  const needsSilhouetteCommands = hasSilhouette;

  this._command = command;

  // None of the derived commands (non-2D) use a different model matrix
  // or bounding volume than the original, so they all point to the
  // ModelDrawCommand's copy to save update time and memory.
  this._modelMatrix = Matrix4.clone(command.modelMatrix);
  this._boundingVolume = BoundingSphere.clone(command.boundingVolume);

  // The 2D model matrix depends on the frame state's map projection,
  // so it must be updated when the commands are handled in pushCommands.
  this._modelMatrix2D = new Matrix4();
  this._boundingVolume2D = new BoundingSphere();
  this._modelMatrix2DDirty = false;

  this._backFaceCulling = command.renderState.cull.enabled;
  this._cullFace = command.renderState.cull.face;
  this._shadows = model.shadows;
  this._debugShowBoundingVolume = command.debugShowBoundingVolume;

  this._usesBackFaceCulling = usesBackFaceCulling;
  this._needsTranslucentCommand = needsTranslucentCommand;
  this._needsSkipLevelOfDetailCommands = needsSkipLevelOfDetailCommands;
  this._needsSilhouetteCommands = needsSilhouetteCommands;

  // Derived commands
  this._originalCommand = undefined;
  this._translucentCommand = undefined;
  this._skipLodBackfaceCommand = undefined;
  this._skipLodStencilCommand = undefined;
  this._silhouetteModelCommand = undefined;
  this._silhouetteColorCommand = undefined;

  // All derived commands (including 2D commands)
  this._derivedCommands = [];
  this._has2DCommands = false;

  initialize(this);
}

function ModelDerivedCommand(options) {
  // The DrawCommand managed by this derived command.
  this.command = options.command;

  // These control whether the derived command should update the
  // values of the DrawCommand for the corresponding properties.
  this.updateShadows = options.updateShadows;
  this.updateBackFaceCulling = options.updateBackFaceCulling;
  this.updateCullFace = options.updateCullFace;
  this.updateDebugShowBoundingVolume = options.updateDebugShowBoundingVolume;

  // Whether this ModelDerivedCommand is in 2D.
  this.is2D = defaultValue(options.is2D, false);

  // A ModelDerivedCommand that is the 2D version of this one.
  this.derivedCommand2D = undefined;
}

ModelDerivedCommand.clone = function (derivedCommand) {
  return new ModelDerivedCommand({
    command: derivedCommand.command,
    updateShadows: derivedCommand.updateShadows,
    updateBackFaceCulling: derivedCommand.updateBackFaceCulling,
    updateCullFace: derivedCommand.updateCullFace,
    updateDebugShowBoundingVolume: derivedCommand.updateDebugShowBoundingVolume,
    is2D: derivedCommand.is2D,
    derivedCommand2D: derivedCommand.derivedCommand2D,
  });
};

function initialize(drawCommand) {
  const command = drawCommand._command;
  command.modelMatrix = drawCommand._modelMatrix;
  command.boundingVolume = drawCommand._boundingVolume;

  const model = drawCommand._model;
  const usesBackFaceCulling = drawCommand._usesBackFaceCulling;
  const derivedCommands = drawCommand._derivedCommands;

  drawCommand._originalCommand = new ModelDerivedCommand({
    command: command,
    updateShadows: true,
    updateBackFaceCulling: usesBackFaceCulling,
    updateCullFace: usesBackFaceCulling,
    updateDebugShowBoundingVolume: true,
    is2D: false,
  });

  derivedCommands.push(drawCommand._originalCommand);

  if (drawCommand._needsTranslucentCommand) {
    drawCommand._translucentCommand = new ModelDerivedCommand({
      command: deriveTranslucentCommand(command),
      updateShadows: true,
      updateBackFaceCulling: false,
      updateCullFace: false,
      updateDebugShowBoundingVolume: true,
    });

    derivedCommands.push(drawCommand._translucentCommand);
  }

  if (drawCommand._needsSkipLevelOfDetailCommands) {
    drawCommand._skipLodBackfaceCommand = new ModelDerivedCommand({
      command: deriveSkipLodBackfaceCommand(command),
      updateShadows: false,
      updateBackFaceCulling: false,
      updateCullFace: usesBackFaceCulling,
      updateDebugShowBoundingVolume: false,
    });

    drawCommand._skipLodStencilCommand = new ModelDerivedCommand({
      command: deriveSkipLodStencilCommand(command, model),
      updateShadows: true,
      updateBackFaceCulling: usesBackFaceCulling,
      updateCullFace: usesBackFaceCulling,
      updateDebugShowBoundingVolume: true,
    });

    derivedCommands.push(drawCommand._skipLodBackfaceCommand);
    derivedCommands.push(drawCommand._skipLodStencilCommand);
  }

  if (drawCommand._needsSilhouetteCommands) {
    drawCommand._silhouetteModelCommand = new ModelDerivedCommand({
      command: deriveSilhouetteModelCommand(command, model),
      updateShadows: true,
      updateBackFaceCulling: usesBackFaceCulling,
      updateCullFace: usesBackFaceCulling,
      updateDebugShowBoundingVolume: true,
    });

    drawCommand._silhouetteColorCommand = new ModelDerivedCommand({
      command: deriveSilhouetteColorCommand(command, model),
      updateShadows: false,
      updateBackFaceCulling: false,
      updateCullFace: false,
      updateDebugShowBoundingVolume: false,
    });

    derivedCommands.push(drawCommand._silhouetteModelCommand);
    derivedCommands.push(drawCommand._silhouetteColorCommand);
  }
}

Object.defineProperties(ModelDrawCommand.prototype, {
  /**
   * The main draw command that the other commands are derived from.
   *
   * @memberof ModelDrawCommand.prototype
   * @type {DrawCommand}
   *
   * @readonly
   * @private
   */
  command: {
    get: function () {
      return this._command;
    },
  },

  /**
   * The runtime primitive that the draw command belongs to.
   *
   * @memberof ModelDrawCommand.prototype
   * @type {ModelRuntimePrimitive}
   *
   * @readonly
   * @private
   */
  runtimePrimitive: {
    get: function () {
      return this._runtimePrimitive;
    },
  },

  /**
   * The model that the draw command belongs to.
   *
   * @memberof ModelDrawCommand.prototype
   * @type {Model}
   *
   * @readonly
   * @private
   */
  model: {
    get: function () {
      return this._model;
    },
  },

  /**
   * The primitive type of the draw command.
   *
   * @memberof ModelDrawCommand.prototype
   * @type {PrimitiveType}
   *
   * @readonly
   * @private
   */
  primitiveType: {
    get: function () {
      return this._command.primitiveType;
    },
  },

  /**
   * The current model matrix applied to the draw commands. If there are
   * 2D draw commands, their model matrix will be derived from the 3D one.
   *
   * @memberof ModelDrawCommand.prototype
   * @type {Matrix4}
   *
   * @readonly
   * @private
   */
  modelMatrix: {
    get: function () {
      return this._modelMatrix;
    },
    set: function (value) {
      this._modelMatrix = Matrix4.clone(value, this._modelMatrix);
      this._modelMatrix2DDirty = true;

      this._boundingVolume = BoundingSphere.transform(
        this.runtimePrimitive.boundingSphere,
        this._modelMatrix,
        this._boundingVolume
      );
    },
  },

  /**
   * The bounding volume of the main draw command. This is equivalent
   * to the primitive's bounding sphere transformed by the draw
   * command's model matrix.
   *
   * @memberof ModelDrawCommand.prototype
   * @type {BoundingSphere}
   *
   * @readonly
   * @private
   */
  boundingVolume: {
    get: function () {
      return this._boundingVolume;
    },
  },

  /**
   * Whether the geometry casts or receives shadows from light sources.
   *
   * @memberof ModelDrawCommand.prototype
   * @type {ShadowMode}
   *
   * @private
   */
  shadows: {
    get: function () {
      return this._shadows;
    },
    set: function (value) {
      this._shadows = value;
      updateShadows(this);
    },
  },

  /**
   * Whether to cull back-facing geometry. When true, back face culling is
   * determined by the material's doubleSided property; when false, back face
   * culling is disabled. Back faces are not culled if the command is
   * translucent.
   *
   * @memberof ModelDrawCommand.prototype
   * @type {boolean}
   *
   * @private
   */
  backFaceCulling: {
    get: function () {
      return this._backFaceCulling;
    },
    set: function (value) {
      if (this._backFaceCulling === value) {
        return;
      }

      this._backFaceCulling = value;
      updateBackFaceCulling(this);
    },
  },

  /**
   * Determines which faces to cull, if culling is enabled.
   *
   * @memberof ModelDrawCommand.prototype
   * @type {CullFace}
   *
   * @private
   */
  cullFace: {
    get: function () {
      return this._cullFace;
    },
    set: function (value) {
      if (this._cullFace === value) {
        return;
      }

      this._cullFace = value;
      updateCullFace(this);
    },
  },

  /**
   * Whether to draw the bounding sphere associated with this draw command.
   *
   * @memberof ModelDrawCommand.prototype
   * @type {boolean}
   *
   * @private
   */
  debugShowBoundingVolume: {
    get: function () {
      return this._debugShowBoundingVolume;
    },
    set: function (value) {
      if (this._debugShowBoundingVolume === value) {
        return;
      }

      this._debugShowBoundingVolume = value;
      updateDebugShowBoundingVolume(this);
    },
  },
});

function updateModelMatrix2D(drawCommand, frameState) {
  const modelMatrix = drawCommand._modelMatrix;
  drawCommand._modelMatrix2D = Matrix4.clone(
    modelMatrix,
    drawCommand._modelMatrix2D
  );

  // Change the translation's y-component so it appears on the opposite side
  // of the map.
  drawCommand._modelMatrix2D[13] -=
    CesiumMath.sign(modelMatrix[13]) *
    2.0 *
    CesiumMath.PI *
    frameState.mapProjection.ellipsoid.maximumRadius;

  drawCommand._boundingVolume2D = BoundingSphere.transform(
    drawCommand.runtimePrimitive.boundingSphere,
    drawCommand._modelMatrix2D,
    drawCommand._boundingVolume2D
  );
}

function updateShadows(drawCommand) {
  const shadows = drawCommand.shadows;
  const castShadows = ShadowMode.castShadows(shadows);
  const receiveShadows = ShadowMode.receiveShadows(shadows);

  const derivedCommands = drawCommand._derivedCommands;

  for (let i = 0; i < derivedCommands.length; ++i) {
    const derivedCommand = derivedCommands[i];
    if (derivedCommand.updateShadows) {
      const command = derivedCommand.command;
      command.castShadows = castShadows;
      command.receiveShadows = receiveShadows;
    }
  }
}

function updateBackFaceCulling(drawCommand) {
  const backFaceCulling = drawCommand.backFaceCulling;
  const derivedCommands = drawCommand._derivedCommands;

  for (let i = 0; i < derivedCommands.length; ++i) {
    const derivedCommand = derivedCommands[i];
    if (derivedCommand.updateBackFaceCulling) {
      const command = derivedCommand.command;
      const renderState = clone(command.renderState, true);
      renderState.cull.enabled = backFaceCulling;
      command.renderState = RenderState.fromCache(renderState);
    }
  }
}

function updateCullFace(drawCommand) {
  const cullFace = drawCommand.cullFace;
  const derivedCommands = drawCommand._derivedCommands;

  for (let i = 0; i < derivedCommands.length; ++i) {
    const derivedCommand = derivedCommands[i];
    if (derivedCommand.updateCullFace) {
      const command = derivedCommand.command;
      const renderState = clone(command.renderState, true);
      renderState.cull.face = cullFace;
      command.renderState = RenderState.fromCache(renderState);
    }
  }
}

function updateDebugShowBoundingVolume(drawCommand) {
  const debugShowBoundingVolume = drawCommand.debugShowBoundingVolume;
  const derivedCommands = drawCommand._derivedCommands;

  for (let i = 0; i < derivedCommands.length; ++i) {
    const derivedCommand = derivedCommands[i];
    if (derivedCommand.updateDebugShowBoundingVolume) {
      const command = derivedCommand.command;
      command.debugShowBoundingVolume = debugShowBoundingVolume;
    }
  }
}

/**
 * Pushes the draw commands necessary to render the primitive.
 * This does not include the draw commands that render its silhouette.
 *
 * @param {FrameState} frameState The frame state.
 * @param {DrawCommand[]} result The array to push the draw commands to.
 *
 * @returns {DrawCommand[]} The modified result parameter.
 *
 * @private
 */
ModelDrawCommand.prototype.pushCommands = function (frameState, result) {
  const use2D = shouldUse2DCommands(this, frameState);

  if (use2D && !this._has2DCommands) {
    derive2DCommands(this);
    this._has2DCommands = true;
    this._modelMatrix2DDirty = true;
  }

  if (this._modelMatrix2DDirty) {
    updateModelMatrix2D(this, frameState);
    this._modelMatrix2DDirty = false;
  }

  const styleCommandsNeeded = this.model.styleCommandsNeeded;
  if (this._needsTranslucentCommand && defined(styleCommandsNeeded)) {
    // StyleCommandsNeeded has three values: all opaque, all translucent, or both.
    if (styleCommandsNeeded !== StyleCommandsNeeded.ALL_OPAQUE) {
      pushCommand(result, this._translucentCommand, use2D);
    }

    // Continue only if opaque commands are needed.
    if (styleCommandsNeeded === StyleCommandsNeeded.ALL_TRANSLUCENT) {
      return;
    }
  }

  if (this._needsSkipLevelOfDetailCommands) {
    const { tileset, tile } = this._model.content;

    if (tileset.hasMixedContent) {
      if (!tile._finalResolution) {
        pushCommand(
          tileset._backfaceCommands,
          this._skipLodBackfaceCommand,
          use2D
        );
      }

      updateSkipLodStencilCommand(this, tile, use2D);
      pushCommand(result, this._skipLodStencilCommand, use2D);
      return;
    }
  }

  if (this._needsSilhouetteCommands) {
    pushCommand(result, this._silhouetteModelCommand, use2D);
    return;
  }

  pushCommand(result, this._originalCommand, use2D);

  return result;
};

/**
 * Pushes the draw commands necessary to render the silhouette. These should
 * be added to the command list after the draw commands of all primitives
 * in the model have been added. This way, the silhouette won't render on
 * top of the model.
 * <p>
 * This should only be called after pushCommands() has been invoked for
 * the ModelDrawCommand this frame. Otherwise, the silhouette commands may
 * not have been derived for 2D. The model matrix will also not have been
 * updated for 2D commands.
 * </p>
 *
 * @param {FrameState} frameState The frame state.
 * @param {DrawCommand[]} result The array to push the silhouette commands to.
 *
 * @returns {DrawCommand[]} The modified result parameter.
 *
 * @private
 */
ModelDrawCommand.prototype.pushSilhouetteCommands = function (
  frameState,
  result
) {
  const use2D = shouldUse2DCommands(this, frameState);
  pushCommand(result, this._silhouetteColorCommand, use2D);

  return result;
};

function pushCommand(commandList, derivedCommand, use2D) {
  commandList.push(derivedCommand.command);
  if (use2D) {
    commandList.push(derivedCommand.derivedCommand2D.command);
  }
}

function shouldUse2DCommands(drawCommand, frameState) {
  if (frameState.mode !== SceneMode.SCENE2D || drawCommand.model._projectTo2D) {
    return false;
  }

  // The draw command's bounding sphere might cause primitives not to render
  // over the IDL, even if they are part of the same model. Use the scene graph's
  // bounding sphere instead.
  const model = drawCommand.model;
  const boundingSphere = model.sceneGraph._boundingSphere2D;

  const left = boundingSphere.center.y - boundingSphere.radius;
  const right = boundingSphere.center.y + boundingSphere.radius;
  const idl2D =
    frameState.mapProjection.ellipsoid.maximumRadius * CesiumMath.PI;

  return (left < idl2D && right > idl2D) || (left < -idl2D && right > -idl2D);
}

function derive2DCommand(drawCommand, derivedCommand) {
  if (!defined(derivedCommand)) {
    return;
  }

  // If the model crosses the IDL in 2D, it will be drawn in one viewport but get
  // clipped by the other viewport. We create a second command that translates
  // the model matrix to the opposite side of the map so the part that was clipped
  // in one viewport is drawn in the other.
  const derivedCommand2D = ModelDerivedCommand.clone(derivedCommand);

  const command2D = DrawCommand.shallowClone(derivedCommand.command);
  command2D.modelMatrix = drawCommand._modelMatrix2D;
  command2D.boundingVolume = drawCommand._boundingVolume2D;

  derivedCommand2D.command = command2D;
  derivedCommand2D.updateShadows = false; // Shadows are disabled for 2D
  derivedCommand2D.is2D = true;

  derivedCommand.derivedCommand2D = derivedCommand2D;
  drawCommand._derivedCommands.push(derivedCommand2D);

  return derivedCommand2D;
}

function derive2DCommands(drawCommand) {
  derive2DCommand(drawCommand, drawCommand._originalCommand);
  derive2DCommand(drawCommand, drawCommand._translucentCommand);
  derive2DCommand(drawCommand, drawCommand._skipLodBackfaceCommand);
  derive2DCommand(drawCommand, drawCommand._skipLodStencilCommand);
  derive2DCommand(drawCommand, drawCommand._silhouetteModelCommand);
  derive2DCommand(drawCommand, drawCommand._silhouetteColorCommand);
}

function deriveTranslucentCommand(command) {
  const derivedCommand = DrawCommand.shallowClone(command);
  derivedCommand.pass = Pass.TRANSLUCENT;
  const rs = clone(command.renderState, true);
  rs.cull.enabled = false;
  rs.depthMask = false;
  rs.blending = BlendingState.ALPHA_BLEND;
  derivedCommand.renderState = RenderState.fromCache(rs);

  return derivedCommand;
}

function deriveSilhouetteModelCommand(command, model) {
  // Wrap around after exceeding the 8-bit stencil limit.
  // The reference is unique to each model until this point.
  const stencilReference = model._silhouetteId % 255;
  const silhouetteModelCommand = DrawCommand.shallowClone(command);
  const renderState = clone(command.renderState, true);

  // Write the reference value into the stencil buffer.
  renderState.stencilTest = {
    enabled: true,
    frontFunction: WebGLConstants.ALWAYS,
    backFunction: WebGLConstants.ALWAYS,
    reference: stencilReference,
    mask: ~0,
    frontOperation: {
      fail: WebGLConstants.KEEP,
      zFail: WebGLConstants.KEEP,
      zPass: WebGLConstants.REPLACE,
    },
    backOperation: {
      fail: WebGLConstants.KEEP,
      zFail: WebGLConstants.KEEP,
      zPass: WebGLConstants.REPLACE,
    },
  };

  if (model.isInvisible()) {
    renderState.colorMask = {
      red: false,
      green: false,
      blue: false,
      alpha: false,
    };
  }

  silhouetteModelCommand.renderState = RenderState.fromCache(renderState);

  return silhouetteModelCommand;
}

function deriveSilhouetteColorCommand(command, model) {
  // Wrap around after exceeding the 8-bit stencil limit.
  // The reference is unique to each model until this point.
  const stencilReference = model._silhouetteId % 255;
  const silhouetteColorCommand = DrawCommand.shallowClone(command);
  const renderState = clone(command.renderState, true);
  renderState.cull.enabled = false;

  // Render the silhouette in the translucent pass if either the command
  // pass or the silhouette color is translucent. This will account for
  // translucent model color, since ModelColorPipelineStage sets the pass
  // to translucent.
  const silhouetteTranslucent =
    command.pass === Pass.TRANSLUCENT || model.silhouetteColor.alpha < 1.0;
  if (silhouetteTranslucent) {
    silhouetteColorCommand.pass = Pass.TRANSLUCENT;
    renderState.depthMask = false;
    renderState.blending = BlendingState.ALPHA_BLEND;
  }

  // Only render the pixels of the silhouette that don't conflict with
  // the stencil buffer. This way, the silhouette doesn't render over
  // the original model.
  renderState.stencilTest = {
    enabled: true,
    frontFunction: WebGLConstants.NOTEQUAL,
    backFunction: WebGLConstants.NOTEQUAL,
    reference: stencilReference,
    mask: ~0,
    frontOperation: {
      fail: WebGLConstants.KEEP,
      zFail: WebGLConstants.KEEP,
      zPass: WebGLConstants.KEEP,
    },
    backOperation: {
      fail: WebGLConstants.KEEP,
      zFail: WebGLConstants.KEEP,
      zPass: WebGLConstants.KEEP,
    },
  };

  const uniformMap = clone(command.uniformMap);
  uniformMap.model_silhouettePass = function () {
    return true;
  };

  silhouetteColorCommand.renderState = RenderState.fromCache(renderState);
  silhouetteColorCommand.uniformMap = uniformMap;
  silhouetteColorCommand.castShadows = false;
  silhouetteColorCommand.receiveShadows = false;

  return silhouetteColorCommand;
}

function updateSkipLodStencilCommand(drawCommand, tile, use2D) {
  const stencilDerivedComand = drawCommand._skipLodStencilCommand;
  const stencilCommand = stencilDerivedComand.command;

  const selectionDepth = tile._selectionDepth;
  const lastSelectionDepth = getLastSelectionDepth(stencilCommand);

  if (selectionDepth !== lastSelectionDepth) {
    const skipLodStencilReference = getStencilReference(selectionDepth);
    const renderState = clone(stencilCommand.renderState, true);
    renderState.stencilTest.reference = skipLodStencilReference;
    stencilCommand.renderState = RenderState.fromCache(renderState);

    if (use2D) {
      stencilDerivedComand.derivedCommand2D.renderState = renderState;
    }
  }
}

function getLastSelectionDepth(stencilCommand) {
  // Isolate the selection depth from the stencil reference.
  const reference = stencilCommand.renderState.stencilTest.reference;
  return (
    (reference & StencilConstants.SKIP_LOD_MASK) >>>
    StencilConstants.SKIP_LOD_BIT_SHIFT
  );
}

function getStencilReference(selectionDepth) {
  // Stencil test is masked to the most significant 3 bits so the reference is shifted.
  // Writes 0 for the terrain bit.
  return (
    StencilConstants.CESIUM_3D_TILE_MASK |
    (selectionDepth << StencilConstants.SKIP_LOD_BIT_SHIFT)
  );
}

function deriveSkipLodBackfaceCommand(command) {
  // Write just backface depth of unresolved tiles so resolved stenciled tiles
  // do not appear in front.
  const backfaceCommand = DrawCommand.shallowClone(command);
  const renderState = clone(command.renderState, true);
  renderState.cull.enabled = true;
  renderState.cull.face = CullFace.FRONT;
  // Back faces do not need to write color.
  renderState.colorMask = {
    red: false,
    green: false,
    blue: false,
    alpha: false,
  };
  // Push back face depth away from the camera so it is less likely that back faces and front faces of the same tile
  // intersect and overlap. This helps avoid flickering for very thin double-sided walls.
  renderState.polygonOffset = {
    enabled: true,
    factor: 5.0,
    units: 5.0,
  };

  // The stencil test is set in TilesetPipelineStage.

  const uniformMap = clone(backfaceCommand.uniformMap);
  const polygonOffset = new Cartesian2(5.0, 5.0);

  uniformMap.u_polygonOffset = function () {
    return polygonOffset;
  };

  backfaceCommand.renderState = RenderState.fromCache(renderState);
  backfaceCommand.uniformMap = uniformMap;
  backfaceCommand.castShadows = false;
  backfaceCommand.receiveShadows = false;

  return backfaceCommand;
}

function deriveSkipLodStencilCommand(command) {
  // Tiles only draw if their selection depth is >= the tile drawn already. They write their
  // selection depth to the stencil buffer to prevent ancestor tiles from drawing on top
  const stencilCommand = DrawCommand.shallowClone(command);
  const renderState = clone(command.renderState, true);
  // The stencil reference is updated dynamically; see updateSkipLodStencilCommand().
  const { stencilTest } = renderState;
  stencilTest.enabled = true;
  stencilTest.mask = StencilConstants.SKIP_LOD_MASK;
  stencilTest.reference = StencilConstants.CESIUM_3D_TILE_MASK;
  stencilTest.frontFunction = StencilFunction.GREATER_OR_EQUAL;
  stencilTest.frontOperation.zPass = StencilOperation.REPLACE;
  stencilTest.backFunction = StencilFunction.GREATER_OR_EQUAL;
  stencilTest.backOperation.zPass = StencilOperation.REPLACE;
  renderState.stencilMask =
    StencilConstants.CESIUM_3D_TILE_MASK | StencilConstants.SKIP_LOD_MASK;

  stencilCommand.renderState = RenderState.fromCache(renderState);

  return stencilCommand;
}

export default ModelDrawCommand;
