import Cartesian2 from "../Core/Cartesian2.js";
import Check from "../Core/Check.js";
import clone from "../Core/clone.js";
import Color from "../Core/Color.js";
import combine from "../Core/combine.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import deprecationWarning from "../Core/deprecationWarning.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import CesiumMath from "../Core/Math.js";
import RuntimeError from "../Core/RuntimeError.js";
import ContextLimits from "../Renderer/ContextLimits.js";
import DrawCommand from "../Renderer/DrawCommand.js";
import Pass from "../Renderer/Pass.js";
import RenderState from "../Renderer/RenderState.js";
import ShaderSource from "../Renderer/ShaderSource.js";
import BatchTexture from "./BatchTexture.js";
import BatchTableHierarchy from "./BatchTableHierarchy.js";
import BlendingState from "./BlendingState.js";
import Cesium3DTileColorBlendMode from "./Cesium3DTileColorBlendMode.js";
import CullFace from "./CullFace.js";
import getBinaryAccessor from "./getBinaryAccessor.js";
import StencilConstants from "./StencilConstants.js";
import StencilFunction from "./StencilFunction.js";
import StencilOperation from "./StencilOperation.js";

const DEFAULT_COLOR_VALUE = BatchTexture.DEFAULT_COLOR_VALUE;
const DEFAULT_SHOW_VALUE = BatchTexture.DEFAULT_SHOW_VALUE;

/**
 * @private
 * @constructor
 */
function Cesium3DTileBatchTable(
  content,
  featuresLength,
  batchTableJson,
  batchTableBinary,
  colorChangedCallback
) {
  /**
   * @readonly
   */
  this.featuresLength = featuresLength;

  let extensions;
  if (defined(batchTableJson)) {
    extensions = batchTableJson.extensions;
  }
  this._extensions = defaultValue(extensions, {});

  const properties = initializeProperties(batchTableJson);
  this._properties = properties;

  this._batchTableHierarchy = initializeHierarchy(
    this,
    batchTableJson,
    batchTableBinary
  );

  const binaryProperties = getBinaryProperties(
    featuresLength,
    properties,
    batchTableBinary
  );
  this._binaryPropertiesByteLength = countBinaryPropertyMemory(
    binaryProperties
  );
  this._batchTableBinaryProperties = binaryProperties;

  this._content = content;

  this._batchTexture = new BatchTexture({
    featuresLength: featuresLength,
    colorChangedCallback: colorChangedCallback,
    owner: content,
    statistics: content.tileset.statistics,
  });
}

// This can be overridden for testing purposes
Cesium3DTileBatchTable._deprecationWarning = deprecationWarning;

Object.defineProperties(Cesium3DTileBatchTable.prototype, {
  /**
   * Size of the batch table, including the batch table hierarchy's binary
   * buffers and any binary properties. JSON data is not counted.
   *
   * @memberof Cesium3DTileBatchTable.prototype
   * @type {Number}
   * @readonly
   * @private
   */
  batchTableByteLength: {
    get: function () {
      let totalByteLength = this._binaryPropertiesByteLength;

      if (defined(this._batchTableHierarchy)) {
        totalByteLength += this._batchTableHierarchy.byteLength;
      }

      totalByteLength += this._batchTexture.byteLength;

      return totalByteLength;
    },
  },
});

function initializeProperties(jsonHeader) {
  const properties = {};

  if (!defined(jsonHeader)) {
    return properties;
  }

  for (const propertyName in jsonHeader) {
    if (
      jsonHeader.hasOwnProperty(propertyName) &&
      propertyName !== "HIERARCHY" && // Deprecated HIERARCHY property
      propertyName !== "extensions" &&
      propertyName !== "extras"
    ) {
      properties[propertyName] = clone(jsonHeader[propertyName], true);
    }
  }

  return properties;
}

function initializeHierarchy(batchTable, jsonHeader, binaryBody) {
  if (!defined(jsonHeader)) {
    return;
  }

  let hierarchy = batchTable._extensions["3DTILES_batch_table_hierarchy"];

  const legacyHierarchy = jsonHeader.HIERARCHY;
  if (defined(legacyHierarchy)) {
    Cesium3DTileBatchTable._deprecationWarning(
      "batchTableHierarchyExtension",
      "The batch table HIERARCHY property has been moved to an extension. Use extensions.3DTILES_batch_table_hierarchy instead."
    );
    batchTable._extensions["3DTILES_batch_table_hierarchy"] = legacyHierarchy;
    hierarchy = legacyHierarchy;
  }

  if (!defined(hierarchy)) {
    return;
  }

  return new BatchTableHierarchy({
    extension: hierarchy,
    binaryBody: binaryBody,
  });
}

function getBinaryProperties(featuresLength, properties, binaryBody) {
  let binaryProperties;
  for (const name in properties) {
    if (properties.hasOwnProperty(name)) {
      const property = properties[name];
      const byteOffset = property.byteOffset;
      if (defined(byteOffset)) {
        // This is a binary property
        const componentType = property.componentType;
        const type = property.type;
        if (!defined(componentType)) {
          throw new RuntimeError("componentType is required.");
        }
        if (!defined(type)) {
          throw new RuntimeError("type is required.");
        }
        if (!defined(binaryBody)) {
          throw new RuntimeError(
            `Property ${name} requires a batch table binary.`
          );
        }

        const binaryAccessor = getBinaryAccessor(property);
        const componentCount = binaryAccessor.componentsPerAttribute;
        const classType = binaryAccessor.classType;
        const typedArray = binaryAccessor.createArrayBufferView(
          binaryBody.buffer,
          binaryBody.byteOffset + byteOffset,
          featuresLength
        );

        if (!defined(binaryProperties)) {
          binaryProperties = {};
        }

        // Store any information needed to access the binary data, including the typed array,
        // componentCount (e.g. a VEC4 would be 4), and the type used to pack and unpack (e.g. Cartesian4).
        binaryProperties[name] = {
          typedArray: typedArray,
          componentCount: componentCount,
          type: classType,
        };
      }
    }
  }
  return binaryProperties;
}

function countBinaryPropertyMemory(binaryProperties) {
  if (!defined(binaryProperties)) {
    return 0;
  }

  let byteLength = 0;
  for (const name in binaryProperties) {
    if (binaryProperties.hasOwnProperty(name)) {
      byteLength += binaryProperties[name].typedArray.byteLength;
    }
  }
  return byteLength;
}

Cesium3DTileBatchTable.getBinaryProperties = function (
  featuresLength,
  batchTableJson,
  batchTableBinary
) {
  return getBinaryProperties(featuresLength, batchTableJson, batchTableBinary);
};

Cesium3DTileBatchTable.prototype.setShow = function (batchId, show) {
  this._batchTexture.setShow(batchId, show);
};

Cesium3DTileBatchTable.prototype.setAllShow = function (show) {
  this._batchTexture.setAllShow(show);
};

Cesium3DTileBatchTable.prototype.getShow = function (batchId) {
  return this._batchTexture.getShow(batchId);
};

Cesium3DTileBatchTable.prototype.setColor = function (batchId, color) {
  this._batchTexture.setColor(batchId, color);
};

Cesium3DTileBatchTable.prototype.setAllColor = function (color) {
  this._batchTexture.setAllColor(color);
};

Cesium3DTileBatchTable.prototype.getColor = function (batchId, result) {
  return this._batchTexture.getColor(batchId, result);
};

Cesium3DTileBatchTable.prototype.getPickColor = function (batchId) {
  return this._batchTexture.getPickColor(batchId);
};

const scratchColor = new Color();

Cesium3DTileBatchTable.prototype.applyStyle = function (style) {
  if (!defined(style)) {
    this.setAllColor(DEFAULT_COLOR_VALUE);
    this.setAllShow(DEFAULT_SHOW_VALUE);
    return;
  }

  const content = this._content;
  const length = this.featuresLength;
  for (let i = 0; i < length; ++i) {
    const feature = content.getFeature(i);
    const color = defined(style.color)
      ? defaultValue(
          style.color.evaluateColor(feature, scratchColor),
          DEFAULT_COLOR_VALUE
        )
      : DEFAULT_COLOR_VALUE;
    const show = defined(style.show)
      ? defaultValue(style.show.evaluate(feature), DEFAULT_SHOW_VALUE)
      : DEFAULT_SHOW_VALUE;
    this.setColor(i, color);
    this.setShow(i, show);
  }
};

function getBinaryProperty(binaryProperty, index) {
  const typedArray = binaryProperty.typedArray;
  const componentCount = binaryProperty.componentCount;
  if (componentCount === 1) {
    return typedArray[index];
  }
  return binaryProperty.type.unpack(typedArray, index * componentCount);
}

function setBinaryProperty(binaryProperty, index, value) {
  const typedArray = binaryProperty.typedArray;
  const componentCount = binaryProperty.componentCount;
  if (componentCount === 1) {
    typedArray[index] = value;
  } else {
    binaryProperty.type.pack(value, typedArray, index * componentCount);
  }
}

function checkBatchId(batchId, featuresLength) {
  if (!defined(batchId) || batchId < 0 || batchId >= featuresLength) {
    throw new DeveloperError(
      `batchId is required and must be between zero and featuresLength - 1 (${featuresLength}` -
        +")."
    );
  }
}

Cesium3DTileBatchTable.prototype.isClass = function (batchId, className) {
  //>>includeStart('debug', pragmas.debug);
  checkBatchId(batchId, this.featuresLength);
  Check.typeOf.string("className", className);
  //>>includeEnd('debug');

  const hierarchy = this._batchTableHierarchy;
  if (!defined(hierarchy)) {
    return false;
  }

  return hierarchy.isClass(batchId, className);
};

Cesium3DTileBatchTable.prototype.isExactClass = function (batchId, className) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.string("className", className);
  //>>includeEnd('debug');

  return this.getExactClassName(batchId) === className;
};

Cesium3DTileBatchTable.prototype.getExactClassName = function (batchId) {
  //>>includeStart('debug', pragmas.debug);
  checkBatchId(batchId, this.featuresLength);
  //>>includeEnd('debug');

  const hierarchy = this._batchTableHierarchy;
  if (!defined(hierarchy)) {
    return undefined;
  }

  return hierarchy.getClassName(batchId);
};

Cesium3DTileBatchTable.prototype.hasProperty = function (batchId, name) {
  //>>includeStart('debug', pragmas.debug);
  checkBatchId(batchId, this.featuresLength);
  Check.typeOf.string("name", name);
  //>>includeEnd('debug');

  return (
    defined(this._properties[name]) ||
    (defined(this._batchTableHierarchy) &&
      this._batchTableHierarchy.hasProperty(batchId, name))
  );
};

/**
 * @private
 */
Cesium3DTileBatchTable.prototype.hasPropertyBySemantic = function () {
  // Cesium 3D Tiles 1.0 formats do not have semantics
  return false;
};

Cesium3DTileBatchTable.prototype.getPropertyIds = function (batchId, results) {
  //>>includeStart('debug', pragmas.debug);
  checkBatchId(batchId, this.featuresLength);
  //>>includeEnd('debug');

  results = defined(results) ? results : [];
  results.length = 0;

  const scratchPropertyIds = Object.keys(this._properties);
  results.push.apply(results, scratchPropertyIds);

  if (defined(this._batchTableHierarchy)) {
    results.push.apply(
      results,
      this._batchTableHierarchy.getPropertyIds(batchId, scratchPropertyIds)
    );
  }

  return results;
};

/**
 * @private
 */
Cesium3DTileBatchTable.prototype.getPropertyBySemantic = function (
  batchId,
  name
) {
  // Cesium 3D Tiles 1.0 formats do not have semantics
  return undefined;
};

Cesium3DTileBatchTable.prototype.getProperty = function (batchId, name) {
  //>>includeStart('debug', pragmas.debug);
  checkBatchId(batchId, this.featuresLength);
  Check.typeOf.string("name", name);
  //>>includeEnd('debug');

  if (defined(this._batchTableBinaryProperties)) {
    const binaryProperty = this._batchTableBinaryProperties[name];
    if (defined(binaryProperty)) {
      return getBinaryProperty(binaryProperty, batchId);
    }
  }

  const propertyValues = this._properties[name];
  if (defined(propertyValues)) {
    return clone(propertyValues[batchId], true);
  }

  if (defined(this._batchTableHierarchy)) {
    const hierarchyProperty = this._batchTableHierarchy.getProperty(
      batchId,
      name
    );
    if (defined(hierarchyProperty)) {
      return hierarchyProperty;
    }
  }

  return undefined;
};

Cesium3DTileBatchTable.prototype.setProperty = function (batchId, name, value) {
  const featuresLength = this.featuresLength;
  //>>includeStart('debug', pragmas.debug);
  checkBatchId(batchId, featuresLength);
  Check.typeOf.string("name", name);
  //>>includeEnd('debug');

  if (defined(this._batchTableBinaryProperties)) {
    const binaryProperty = this._batchTableBinaryProperties[name];
    if (defined(binaryProperty)) {
      setBinaryProperty(binaryProperty, batchId, value);
      return;
    }
  }

  if (defined(this._batchTableHierarchy)) {
    if (this._batchTableHierarchy.setProperty(batchId, name, value)) {
      return;
    }
  }

  let propertyValues = this._properties[name];
  if (!defined(propertyValues)) {
    // Property does not exist. Create it.
    this._properties[name] = new Array(featuresLength);
    propertyValues = this._properties[name];
  }

  propertyValues[batchId] = clone(value, true);
};

function getGlslComputeSt(batchTable) {
  // GLSL batchId is zero-based: [0, featuresLength - 1]
  if (batchTable._batchTexture.textureDimensions.y === 1) {
    return (
      "uniform vec4 tile_textureStep; \n" +
      "vec2 computeSt(float batchId) \n" +
      "{ \n" +
      "    float stepX = tile_textureStep.x; \n" +
      "    float centerX = tile_textureStep.y; \n" +
      "    return vec2(centerX + (batchId * stepX), 0.5); \n" +
      "} \n"
    );
  }

  return (
    "uniform vec4 tile_textureStep; \n" +
    "uniform vec2 tile_textureDimensions; \n" +
    "vec2 computeSt(float batchId) \n" +
    "{ \n" +
    "    float stepX = tile_textureStep.x; \n" +
    "    float centerX = tile_textureStep.y; \n" +
    "    float stepY = tile_textureStep.z; \n" +
    "    float centerY = tile_textureStep.w; \n" +
    "    float xId = mod(batchId, tile_textureDimensions.x); \n" +
    "    float yId = floor(batchId / tile_textureDimensions.x); \n" +
    "    return vec2(centerX + (xId * stepX), centerY + (yId * stepY)); \n" +
    "} \n"
  );
}

Cesium3DTileBatchTable.prototype.getVertexShaderCallback = function (
  handleTranslucent,
  batchIdAttributeName,
  diffuseAttributeOrUniformName
) {
  if (this.featuresLength === 0) {
    return;
  }

  const that = this;
  return function (source) {
    // If the color blend mode is HIGHLIGHT, the highlight color will always be applied in the fragment shader.
    // No need to apply the highlight color in the vertex shader as well.
    const renamedSource = modifyDiffuse(
      source,
      diffuseAttributeOrUniformName,
      false
    );
    let newMain;

    if (ContextLimits.maximumVertexTextureImageUnits > 0) {
      // When VTF is supported, perform per-feature show/hide in the vertex shader
      newMain = "";
      if (handleTranslucent) {
        newMain += "uniform bool tile_translucentCommand; \n";
      }
      newMain +=
        `${
          "uniform sampler2D tile_batchTexture; \n" +
          "varying vec4 tile_featureColor; \n" +
          "varying vec2 tile_featureSt; \n" +
          "void main() \n" +
          "{ \n" +
          "    vec2 st = computeSt("
        }${batchIdAttributeName}); \n` +
        `    vec4 featureProperties = texture2D(tile_batchTexture, st); \n` +
        `    tile_color(featureProperties); \n` +
        `    float show = ceil(featureProperties.a); \n` + // 0 - false, non-zero - true
        `    gl_Position *= show; \n`; // Per-feature show/hide
      if (handleTranslucent) {
        newMain +=
          "    bool isStyleTranslucent = (featureProperties.a != 1.0); \n" +
          "    if (czm_pass == czm_passTranslucent) \n" +
          "    { \n" +
          "        if (!isStyleTranslucent && !tile_translucentCommand) \n" + // Do not render opaque features in the translucent pass
          "        { \n" +
          "            gl_Position *= 0.0; \n" +
          "        } \n" +
          "    } \n" +
          "    else \n" +
          "    { \n" +
          "        if (isStyleTranslucent) \n" + // Do not render translucent features in the opaque pass
          "        { \n" +
          "            gl_Position *= 0.0; \n" +
          "        } \n" +
          "    } \n";
      }
      newMain +=
        "    tile_featureColor = featureProperties; \n" +
        "    tile_featureSt = st; \n" +
        "}";
    } else {
      // When VTF is not supported, color blend mode MIX will look incorrect due to the feature's color not being available in the vertex shader
      newMain =
        `${
          "varying vec2 tile_featureSt; \n" +
          "void main() \n" +
          "{ \n" +
          "    tile_color(vec4(1.0)); \n" +
          "    tile_featureSt = computeSt("
        }${batchIdAttributeName}); \n` + `}`;
    }

    return `${renamedSource}\n${getGlslComputeSt(that)}${newMain}`;
  };
};

function getDefaultShader(source, applyHighlight) {
  source = ShaderSource.replaceMain(source, "tile_main");

  if (!applyHighlight) {
    return (
      `${source}void tile_color(vec4 tile_featureColor) \n` +
      `{ \n` +
      `    tile_main(); \n` +
      `} \n`
    );
  }

  // The color blend mode is intended for the RGB channels so alpha is always just multiplied.
  // gl_FragColor is multiplied by the tile color only when tile_colorBlend is 0.0 (highlight)
  return (
    `${source}uniform float tile_colorBlend; \n` +
    `void tile_color(vec4 tile_featureColor) \n` +
    `{ \n` +
    `    tile_main(); \n` +
    `    tile_featureColor = czm_gammaCorrect(tile_featureColor); \n` +
    `    gl_FragColor.a *= tile_featureColor.a; \n` +
    `    float highlight = ceil(tile_colorBlend); \n` +
    `    gl_FragColor.rgb *= mix(tile_featureColor.rgb, vec3(1.0), highlight); \n` +
    `} \n`
  );
}

function replaceDiffuseTextureCalls(source, diffuseAttributeOrUniformName) {
  const functionCall = `texture2D(${diffuseAttributeOrUniformName}`;

  let fromIndex = 0;
  let startIndex = source.indexOf(functionCall, fromIndex);
  let endIndex;

  while (startIndex > -1) {
    let nestedLevel = 0;
    for (let i = startIndex; i < source.length; ++i) {
      const character = source.charAt(i);
      if (character === "(") {
        ++nestedLevel;
      } else if (character === ")") {
        --nestedLevel;
        if (nestedLevel === 0) {
          endIndex = i + 1;
          break;
        }
      }
    }
    const extractedFunction = source.slice(startIndex, endIndex);
    const replacedFunction = `tile_diffuse_final(${extractedFunction}, tile_diffuse)`;

    source =
      source.slice(0, startIndex) + replacedFunction + source.slice(endIndex);
    fromIndex = startIndex + replacedFunction.length;
    startIndex = source.indexOf(functionCall, fromIndex);
  }

  return source;
}

function modifyDiffuse(source, diffuseAttributeOrUniformName, applyHighlight) {
  // If the glTF does not specify the _3DTILESDIFFUSE semantic, return the default shader.
  // Otherwise if _3DTILESDIFFUSE is defined prefer the shader below that can switch the color mode at runtime.
  if (!defined(diffuseAttributeOrUniformName)) {
    return getDefaultShader(source, applyHighlight);
  }

  // Find the diffuse uniform. Examples matches:
  //   uniform vec3 u_diffuseColor;
  //   uniform sampler2D diffuseTexture;
  let regex = new RegExp(
    `(uniform|attribute|in)\\s+(vec[34]|sampler2D)\\s+${diffuseAttributeOrUniformName};`
  );
  const uniformMatch = source.match(regex);

  if (!defined(uniformMatch)) {
    // Could not find uniform declaration of type vec3, vec4, or sampler2D
    return getDefaultShader(source, applyHighlight);
  }

  const declaration = uniformMatch[0];
  const type = uniformMatch[2];

  source = ShaderSource.replaceMain(source, "tile_main");
  source = source.replace(declaration, ""); // Remove uniform declaration for now so the replace below doesn't affect it

  // If the tile color is white, use the source color. This implies the feature has not been styled.
  // Highlight: tile_colorBlend is 0.0 and the source color is used
  // Replace: tile_colorBlend is 1.0 and the tile color is used
  // Mix: tile_colorBlend is between 0.0 and 1.0, causing the source color and tile color to mix
  const finalDiffuseFunction =
    "bool isWhite(vec3 color) \n" +
    "{ \n" +
    "    return all(greaterThan(color, vec3(1.0 - czm_epsilon3))); \n" +
    "} \n" +
    "vec4 tile_diffuse_final(vec4 sourceDiffuse, vec4 tileDiffuse) \n" +
    "{ \n" +
    "    vec4 blendDiffuse = mix(sourceDiffuse, tileDiffuse, tile_colorBlend); \n" +
    "    vec4 diffuse = isWhite(tileDiffuse.rgb) ? sourceDiffuse : blendDiffuse; \n" +
    "    return vec4(diffuse.rgb, sourceDiffuse.a); \n" +
    "} \n";

  // The color blend mode is intended for the RGB channels so alpha is always just multiplied.
  // gl_FragColor is multiplied by the tile color only when tile_colorBlend is 0.0 (highlight)
  const highlight =
    "    tile_featureColor = czm_gammaCorrect(tile_featureColor); \n" +
    "    gl_FragColor.a *= tile_featureColor.a; \n" +
    "    float highlight = ceil(tile_colorBlend); \n" +
    "    gl_FragColor.rgb *= mix(tile_featureColor.rgb, vec3(1.0), highlight); \n";

  let setColor;
  if (type === "vec3" || type === "vec4") {
    const sourceDiffuse =
      type === "vec3"
        ? `vec4(${diffuseAttributeOrUniformName}, 1.0)`
        : diffuseAttributeOrUniformName;
    const replaceDiffuse =
      type === "vec3" ? "tile_diffuse.xyz" : "tile_diffuse";
    regex = new RegExp(diffuseAttributeOrUniformName, "g");
    source = source.replace(regex, replaceDiffuse);
    setColor =
      `    vec4 source = ${sourceDiffuse}; \n` +
      `    tile_diffuse = tile_diffuse_final(source, tile_featureColor); \n` +
      `    tile_main(); \n`;
  } else if (type === "sampler2D") {
    // Handles any number of nested parentheses
    // E.g. texture2D(u_diffuse, uv)
    // E.g. texture2D(u_diffuse, computeUV(index))
    source = replaceDiffuseTextureCalls(source, diffuseAttributeOrUniformName);
    setColor =
      "    tile_diffuse = tile_featureColor; \n" + "    tile_main(); \n";
  }

  source =
    `${
      "uniform float tile_colorBlend; \n" + "vec4 tile_diffuse = vec4(1.0); \n"
    }${finalDiffuseFunction}${declaration}\n${source}\n` +
    `void tile_color(vec4 tile_featureColor) \n` +
    `{ \n${setColor}`;

  if (applyHighlight) {
    source += highlight;
  }

  source += "} \n";
  return source;
}

Cesium3DTileBatchTable.prototype.getFragmentShaderCallback = function (
  handleTranslucent,
  diffuseAttributeOrUniformName,
  hasPremultipliedAlpha
) {
  if (this.featuresLength === 0) {
    return;
  }
  return function (source) {
    source = modifyDiffuse(source, diffuseAttributeOrUniformName, true);
    if (ContextLimits.maximumVertexTextureImageUnits > 0) {
      // When VTF is supported, per-feature show/hide already happened in the fragment shader
      source +=
        "uniform sampler2D tile_pickTexture; \n" +
        "varying vec2 tile_featureSt; \n" +
        "varying vec4 tile_featureColor; \n" +
        "void main() \n" +
        "{ \n" +
        "    tile_color(tile_featureColor); \n";

      if (hasPremultipliedAlpha) {
        source += "    gl_FragColor.rgb *= gl_FragColor.a; \n";
      }

      source += "}";
    } else {
      if (handleTranslucent) {
        source += "uniform bool tile_translucentCommand; \n";
      }
      source +=
        "uniform sampler2D tile_pickTexture; \n" +
        "uniform sampler2D tile_batchTexture; \n" +
        "varying vec2 tile_featureSt; \n" +
        "void main() \n" +
        "{ \n" +
        "    vec4 featureProperties = texture2D(tile_batchTexture, tile_featureSt); \n" +
        "    if (featureProperties.a == 0.0) { \n" + // show: alpha == 0 - false, non-zeo - true
        "        discard; \n" +
        "    } \n";

      if (handleTranslucent) {
        source +=
          "    bool isStyleTranslucent = (featureProperties.a != 1.0); \n" +
          "    if (czm_pass == czm_passTranslucent) \n" +
          "    { \n" +
          "        if (!isStyleTranslucent && !tile_translucentCommand) \n" + // Do not render opaque features in the translucent pass
          "        { \n" +
          "            discard; \n" +
          "        } \n" +
          "    } \n" +
          "    else \n" +
          "    { \n" +
          "        if (isStyleTranslucent) \n" + // Do not render translucent features in the opaque pass
          "        { \n" +
          "            discard; \n" +
          "        } \n" +
          "    } \n";
      }

      source += "    tile_color(featureProperties); \n";

      if (hasPremultipliedAlpha) {
        source += "    gl_FragColor.rgb *= gl_FragColor.a; \n";
      }

      source += "} \n";
    }
    return source;
  };
};

Cesium3DTileBatchTable.prototype.getClassificationFragmentShaderCallback = function () {
  if (this.featuresLength === 0) {
    return;
  }
  return function (source) {
    source = ShaderSource.replaceMain(source, "tile_main");
    if (ContextLimits.maximumVertexTextureImageUnits > 0) {
      // When VTF is supported, per-feature show/hide already happened in the fragment shader
      source +=
        "uniform sampler2D tile_pickTexture;\n" +
        "varying vec2 tile_featureSt; \n" +
        "varying vec4 tile_featureColor; \n" +
        "void main() \n" +
        "{ \n" +
        "    tile_main(); \n" +
        "    gl_FragColor = tile_featureColor; \n" +
        "    gl_FragColor.rgb *= gl_FragColor.a; \n" +
        "}";
    } else {
      source +=
        "uniform sampler2D tile_batchTexture; \n" +
        "uniform sampler2D tile_pickTexture;\n" +
        "varying vec2 tile_featureSt; \n" +
        "void main() \n" +
        "{ \n" +
        "    tile_main(); \n" +
        "    vec4 featureProperties = texture2D(tile_batchTexture, tile_featureSt); \n" +
        "    if (featureProperties.a == 0.0) { \n" + // show: alpha == 0 - false, non-zero - true
        "        discard; \n" +
        "    } \n" +
        "    gl_FragColor = featureProperties; \n" +
        "    gl_FragColor.rgb *= gl_FragColor.a; \n" +
        "} \n";
    }
    return source;
  };
};

function getColorBlend(batchTable) {
  const tileset = batchTable._content.tileset;
  const colorBlendMode = tileset.colorBlendMode;
  const colorBlendAmount = tileset.colorBlendAmount;
  if (colorBlendMode === Cesium3DTileColorBlendMode.HIGHLIGHT) {
    return 0.0;
  }
  if (colorBlendMode === Cesium3DTileColorBlendMode.REPLACE) {
    return 1.0;
  }
  if (colorBlendMode === Cesium3DTileColorBlendMode.MIX) {
    // The value 0.0 is reserved for highlight, so clamp to just above 0.0.
    return CesiumMath.clamp(colorBlendAmount, CesiumMath.EPSILON4, 1.0);
  }
  //>>includeStart('debug', pragmas.debug);
  throw new DeveloperError(`Invalid color blend mode "${colorBlendMode}".`);
  //>>includeEnd('debug');
}

Cesium3DTileBatchTable.prototype.getUniformMapCallback = function () {
  if (this.featuresLength === 0) {
    return;
  }

  const that = this;
  return function (uniformMap) {
    const batchUniformMap = {
      tile_batchTexture: function () {
        // PERFORMANCE_IDEA: we could also use a custom shader that avoids the texture read.
        return defaultValue(
          that._batchTexture.batchTexture,
          that._batchTexture.defaultTexture
        );
      },
      tile_textureDimensions: function () {
        return that._batchTexture.textureDimensions;
      },
      tile_textureStep: function () {
        return that._batchTexture.textureStep;
      },
      tile_colorBlend: function () {
        return getColorBlend(that);
      },
      tile_pickTexture: function () {
        return that._batchTexture.pickTexture;
      },
    };

    return combine(uniformMap, batchUniformMap);
  };
};

Cesium3DTileBatchTable.prototype.getPickId = function () {
  return "texture2D(tile_pickTexture, tile_featureSt)";
};

///////////////////////////////////////////////////////////////////////////

const StyleCommandsNeeded = {
  ALL_OPAQUE: 0,
  ALL_TRANSLUCENT: 1,
  OPAQUE_AND_TRANSLUCENT: 2,
};

Cesium3DTileBatchTable.prototype.addDerivedCommands = function (
  frameState,
  commandStart
) {
  const commandList = frameState.commandList;
  const commandEnd = commandList.length;
  const tile = this._content._tile;
  const finalResolution = tile._finalResolution;
  const tileset = tile.tileset;
  const bivariateVisibilityTest =
    tileset._skipLevelOfDetail &&
    tileset._hasMixedContent &&
    frameState.context.stencilBuffer;
  const styleCommandsNeeded = getStyleCommandsNeeded(this);

  for (let i = commandStart; i < commandEnd; ++i) {
    const command = commandList[i];
    if (command.pass === Pass.COMPUTE) {
      continue;
    }

    let derivedCommands = command.derivedCommands.tileset;
    if (!defined(derivedCommands) || command.dirty) {
      derivedCommands = {};
      command.derivedCommands.tileset = derivedCommands;
      derivedCommands.originalCommand = deriveCommand(command);
      command.dirty = false;
    }
    const originalCommand = derivedCommands.originalCommand;

    if (
      styleCommandsNeeded !== StyleCommandsNeeded.ALL_OPAQUE &&
      command.pass !== Pass.TRANSLUCENT
    ) {
      if (!defined(derivedCommands.translucent)) {
        derivedCommands.translucent = deriveTranslucentCommand(originalCommand);
      }
    }

    if (
      styleCommandsNeeded !== StyleCommandsNeeded.ALL_TRANSLUCENT &&
      command.pass !== Pass.TRANSLUCENT
    ) {
      if (!defined(derivedCommands.opaque)) {
        derivedCommands.opaque = deriveOpaqueCommand(originalCommand);
      }

      if (bivariateVisibilityTest) {
        if (!finalResolution) {
          if (!defined(derivedCommands.zback)) {
            derivedCommands.zback = deriveZBackfaceCommand(
              frameState.context,
              originalCommand
            );
          }
          tileset._backfaceCommands.push(derivedCommands.zback);
        }
        if (
          !defined(derivedCommands.stencil) ||
          tile._selectionDepth !==
            getLastSelectionDepth(derivedCommands.stencil)
        ) {
          if (command.renderState.depthMask) {
            derivedCommands.stencil = deriveStencilCommand(
              originalCommand,
              tile._selectionDepth
            );
          } else {
            // Ignore if tile does not write depth
            derivedCommands.stencil = derivedCommands.opaque;
          }
        }
      }
    }

    const opaqueCommand = bivariateVisibilityTest
      ? derivedCommands.stencil
      : derivedCommands.opaque;
    const translucentCommand = derivedCommands.translucent;

    // If the command was originally opaque:
    //    * If the styling applied to the tile is all opaque, use the opaque command
    //      (with one additional uniform needed for the shader).
    //    * If the styling is all translucent, use new (cached) derived commands (front
    //      and back faces) with a translucent render state.
    //    * If the styling causes both opaque and translucent features in this tile,
    //      then use both sets of commands.
    if (command.pass !== Pass.TRANSLUCENT) {
      if (styleCommandsNeeded === StyleCommandsNeeded.ALL_OPAQUE) {
        commandList[i] = opaqueCommand;
      }
      if (styleCommandsNeeded === StyleCommandsNeeded.ALL_TRANSLUCENT) {
        commandList[i] = translucentCommand;
      }
      if (styleCommandsNeeded === StyleCommandsNeeded.OPAQUE_AND_TRANSLUCENT) {
        // PERFORMANCE_IDEA: if the tile has multiple commands, we do not know what features are in what
        // commands so this case may be overkill.
        commandList[i] = opaqueCommand;
        commandList.push(translucentCommand);
      }
    } else {
      // Command was originally translucent so no need to derive new commands;
      // as of now, a style can't change an originally translucent feature to
      // opaque since the style's alpha is modulated, not a replacement.  When
      // this changes, we need to derive new opaque commands here.
      commandList[i] = originalCommand;
    }
  }
};

function getStyleCommandsNeeded(batchTable) {
  const translucentFeaturesLength =
    batchTable._batchTexture.translucentFeaturesLength;

  if (translucentFeaturesLength === 0) {
    return StyleCommandsNeeded.ALL_OPAQUE;
  } else if (translucentFeaturesLength === batchTable.featuresLength) {
    return StyleCommandsNeeded.ALL_TRANSLUCENT;
  }

  return StyleCommandsNeeded.OPAQUE_AND_TRANSLUCENT;
}

function deriveCommand(command) {
  const derivedCommand = DrawCommand.shallowClone(command);

  // Add a uniform to indicate if the original command was translucent so
  // the shader knows not to cull vertices that were originally transparent
  // even though their style is opaque.
  const translucentCommand = derivedCommand.pass === Pass.TRANSLUCENT;

  derivedCommand.uniformMap = defined(derivedCommand.uniformMap)
    ? derivedCommand.uniformMap
    : {};
  derivedCommand.uniformMap.tile_translucentCommand = function () {
    return translucentCommand;
  };

  return derivedCommand;
}

function deriveTranslucentCommand(command) {
  const derivedCommand = DrawCommand.shallowClone(command);
  derivedCommand.pass = Pass.TRANSLUCENT;
  derivedCommand.renderState = getTranslucentRenderState(command.renderState);
  return derivedCommand;
}

function deriveOpaqueCommand(command) {
  const derivedCommand = DrawCommand.shallowClone(command);
  derivedCommand.renderState = getOpaqueRenderState(command.renderState);
  return derivedCommand;
}

function getLogDepthPolygonOffsetFragmentShaderProgram(context, shaderProgram) {
  let shader = context.shaderCache.getDerivedShaderProgram(
    shaderProgram,
    "zBackfaceLogDepth"
  );
  if (!defined(shader)) {
    const fs = shaderProgram.fragmentShaderSource.clone();
    fs.defines = defined(fs.defines) ? fs.defines.slice(0) : [];
    fs.defines.push("POLYGON_OFFSET");

    fs.sources.unshift(
      "#ifdef GL_OES_standard_derivatives\n#extension GL_OES_standard_derivatives : enable\n#endif\n"
    );
    shader = context.shaderCache.createDerivedShaderProgram(
      shaderProgram,
      "zBackfaceLogDepth",
      {
        vertexShaderSource: shaderProgram.vertexShaderSource,
        fragmentShaderSource: fs,
        attributeLocations: shaderProgram._attributeLocations,
      }
    );
  }

  return shader;
}

function deriveZBackfaceCommand(context, command) {
  // Write just backface depth of unresolved tiles so resolved stenciled tiles do not appear in front
  const derivedCommand = DrawCommand.shallowClone(command);
  const rs = clone(derivedCommand.renderState, true);
  rs.cull.enabled = true;
  rs.cull.face = CullFace.FRONT;
  // Back faces do not need to write color.
  rs.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.
  rs.polygonOffset = {
    enabled: true,
    factor: 5.0,
    units: 5.0,
  };
  // Set the 3D Tiles bit
  rs.stencilTest = StencilConstants.setCesium3DTileBit();
  rs.stencilMask = StencilConstants.CESIUM_3D_TILE_MASK;

  derivedCommand.renderState = RenderState.fromCache(rs);
  derivedCommand.castShadows = false;
  derivedCommand.receiveShadows = false;
  derivedCommand.uniformMap = clone(command.uniformMap);

  const polygonOffset = new Cartesian2(5.0, 5.0);
  derivedCommand.uniformMap.u_polygonOffset = function () {
    return polygonOffset;
  };

  // Make the log depth depth fragment write account for the polygon offset, too.
  // Otherwise, the back face commands will cause the higher resolution
  // tiles to disappear.
  derivedCommand.shaderProgram = getLogDepthPolygonOffsetFragmentShaderProgram(
    context,
    command.shaderProgram
  );
  return derivedCommand;
}

function deriveStencilCommand(command, reference) {
  // 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 derivedCommand = DrawCommand.shallowClone(command);
  const rs = clone(derivedCommand.renderState, true);
  // Stencil test is masked to the most significant 3 bits so the reference is shifted. Writes 0 for the terrain bit
  rs.stencilTest.enabled = true;
  rs.stencilTest.mask = StencilConstants.SKIP_LOD_MASK;
  rs.stencilTest.reference =
    StencilConstants.CESIUM_3D_TILE_MASK |
    (reference << StencilConstants.SKIP_LOD_BIT_SHIFT);
  rs.stencilTest.frontFunction = StencilFunction.GREATER_OR_EQUAL;
  rs.stencilTest.frontOperation.zPass = StencilOperation.REPLACE;
  rs.stencilTest.backFunction = StencilFunction.GREATER_OR_EQUAL;
  rs.stencilTest.backOperation.zPass = StencilOperation.REPLACE;
  rs.stencilMask =
    StencilConstants.CESIUM_3D_TILE_MASK | StencilConstants.SKIP_LOD_MASK;
  derivedCommand.renderState = RenderState.fromCache(rs);
  return derivedCommand;
}

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 getTranslucentRenderState(renderState) {
  const rs = clone(renderState, true);
  rs.cull.enabled = false;
  rs.depthTest.enabled = true;
  rs.depthMask = false;
  rs.blending = BlendingState.ALPHA_BLEND;
  rs.stencilTest = StencilConstants.setCesium3DTileBit();
  rs.stencilMask = StencilConstants.CESIUM_3D_TILE_MASK;

  return RenderState.fromCache(rs);
}

function getOpaqueRenderState(renderState) {
  const rs = clone(renderState, true);
  rs.stencilTest = StencilConstants.setCesium3DTileBit();
  rs.stencilMask = StencilConstants.CESIUM_3D_TILE_MASK;

  return RenderState.fromCache(rs);
}

Cesium3DTileBatchTable.prototype.update = function (tileset, frameState) {
  this._batchTexture.update(tileset, frameState);
};

Cesium3DTileBatchTable.prototype.isDestroyed = function () {
  return false;
};

Cesium3DTileBatchTable.prototype.destroy = function () {
  this._batchTexture = this._batchTexture && this._batchTexture.destroy();
  return destroyObject(this);
};
export default Cesium3DTileBatchTable;
