import Check from "../Core/Check.js";
import defaultValue from "../Core/defaultValue.js";
import DeveloperError from "../Core/DeveloperError.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import getJsonFromTypedArray from "../Core/getJsonFromTypedArray.js";
import RuntimeError from "../Core/RuntimeError.js";
import hasExtension from "./hasExtension.js";
import ImplicitAvailabilityBitstream from "./ImplicitAvailabilityBitstream.js";
import ImplicitMetadataView from "./ImplicitMetadataView.js";
import ImplicitSubdivisionScheme from "./ImplicitSubdivisionScheme.js";
import ImplicitSubtreeMetadata from "./ImplicitSubtreeMetadata.js";
import MetadataTable from "./MetadataTable.js";
import ResourceCache from "./ResourceCache.js";

/**
 * An object representing a single subtree in an implicit tileset
 * including availability.
 * <p>
 * Subtrees handle tile metadata, defined in the subtree JSON in either
 * tileMetadata (3D Tiles 1.1) or the <code>3DTILES_metadata</code> extension.
 * Subtrees also handle content metadata and metadata about the subtree itself.
 * </p>
 *
 * This object is normally not instantiated directly, use {@link ImplicitSubtree.fromSubtreeJson}.
 *
 * @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_metadata#implicit-tile-properties|Implicit Tile Properties in the 3DTILES_metadata specification}
 * @see ImplicitSubtree.fromSubtreeJson
 *
 * @alias ImplicitSubtree
 * @constructor
 *
 * @param {Resource} resource The resource for this subtree. This is used for fetching external buffers as needed.
 * @param {ImplicitTileset} implicitTileset The implicit tileset. This includes information about the size of subtrees
 * @param {ImplicitTileCoordinates} implicitCoordinates The coordinates of the subtree's root tile.
 *
 * @private
 * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
 */
function ImplicitSubtree(resource, implicitTileset, implicitCoordinates) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("resource", resource);
  Check.typeOf.object("implicitTileset", implicitTileset);
  Check.typeOf.object("implicitCoordinates", implicitCoordinates);
  //>>includeEnd('debug');

  this._resource = resource;
  this._subtreeJson = undefined;
  this._bufferLoader = undefined;
  this._tileAvailability = undefined;
  this._contentAvailabilityBitstreams = [];
  this._childSubtreeAvailability = undefined;
  this._implicitCoordinates = implicitCoordinates;
  this._subtreeLevels = implicitTileset.subtreeLevels;
  this._subdivisionScheme = implicitTileset.subdivisionScheme;
  this._branchingFactor = implicitTileset.branchingFactor;

  // properties for metadata
  this._metadata = undefined;
  this._tileMetadataTable = undefined;
  this._tilePropertyTableJson = undefined;

  this._contentMetadataTables = [];
  this._contentPropertyTableJsons = [];

  // Jump buffers are maps of availability bit index to entity ID
  this._tileJumpBuffer = undefined;
  this._contentJumpBuffers = [];

  this._ready = false;
}

Object.defineProperties(ImplicitSubtree.prototype, {
  /**
   * Returns true once all necessary availability buffers
   * are loaded.
   *
   * @type {boolean}
   * @readonly
   * @private
   */
  ready: {
    get: function () {
      return this._ready;
    },
  },

  /**
   * When subtree metadata is present (3D Tiles 1.1), this property stores an {@link ImplicitSubtreeMetadata} instance
   *
   * @type {ImplicitSubtreeMetadata}
   * @readonly
   * @private
   */
  metadata: {
    get: function () {
      return this._metadata;
    },
  },

  /**
   * When tile metadata is present (3D Tiles 1.1) or the <code>3DTILES_metadata</code> extension is used,
   * this property stores a {@link MetadataTable} instance for the tiles in the subtree.
   *
   * @type {MetadataTable}
   * @readonly
   * @private
   */
  tileMetadataTable: {
    get: function () {
      return this._tileMetadataTable;
    },
  },

  /**
   * When tile metadata is present (3D Tiles 1.1) or the <code>3DTILES_metadata</code> extension is used,
   * this property stores the JSON from the extension. This is used by {@link TileMetadata}
   * to get the extras and extensions for the tiles in the subtree.
   *
   * @type {object}
   * @readonly
   * @private
   */
  tilePropertyTableJson: {
    get: function () {
      return this._tilePropertyTableJson;
    },
  },

  /**
   * When content metadata is present (3D Tiles 1.1), this property stores
   * an array of {@link MetadataTable} instances for the contents in the subtree.
   *
   * @type {Array}
   * @readonly
   * @private
   */
  contentMetadataTables: {
    get: function () {
      return this._contentMetadataTables;
    },
  },

  /**
   * When content metadata is present (3D Tiles 1.1), this property
   * an array of the JSONs from the extension. This is used to get the extras
   * and extensions for the contents in the subtree.
   *
   * @type {Array}
   * @readonly
   * @private
   */
  contentPropertyTableJsons: {
    get: function () {
      return this._contentPropertyTableJsons;
    },
  },

  /**
   * Gets the implicit tile coordinates for the root of the subtree.
   *
   * @type {ImplicitTileCoordinates}
   * @readonly
   * @private
   */
  implicitCoordinates: {
    get: function () {
      return this._implicitCoordinates;
    },
  },
});

/**
 * Check if a specific tile is available at an index of the tile availability bitstream
 *
 * @param {number} index The index of the desired tile
 * @returns {boolean} The value of the i-th bit
 * @private
 */
ImplicitSubtree.prototype.tileIsAvailableAtIndex = function (index) {
  return this._tileAvailability.getBit(index);
};

/**
 * Check if a specific tile is available at an implicit tile coordinate
 *
 * @param {ImplicitTileCoordinates} implicitCoordinates The global coordinates of a tile
 * @returns {boolean} The value of the i-th bit
 * @private
 */
ImplicitSubtree.prototype.tileIsAvailableAtCoordinates = function (
  implicitCoordinates
) {
  const index = this.getTileIndex(implicitCoordinates);
  return this.tileIsAvailableAtIndex(index);
};

/**
 * Check if a specific tile's content is available at an index of the content availability bitstream
 *
 * @param {number} index The index of the desired tile
 * @param {number} [contentIndex=0] The index of the desired content when multiple contents are used.
 * @returns {boolean} The value of the i-th bit
 * @private
 */
ImplicitSubtree.prototype.contentIsAvailableAtIndex = function (
  index,
  contentIndex
) {
  contentIndex = defaultValue(contentIndex, 0);
  //>>includeStart('debug', pragmas.debug);
  if (
    contentIndex < 0 ||
    contentIndex >= this._contentAvailabilityBitstreams.length
  ) {
    throw new DeveloperError("contentIndex out of bounds.");
  }
  //>>includeEnd('debug');

  return this._contentAvailabilityBitstreams[contentIndex].getBit(index);
};

/**
 * Check if a specific tile's content is available at an implicit tile coordinate
 *
 * @param {ImplicitTileCoordinates} implicitCoordinates The global coordinates of a tile
 * @param {number} [contentIndex=0] The index of the desired content when the <code>3DTILES_multiple_contents</code> extension is used.
 * @returns {boolean} The value of the i-th bit
 * @private
 */
ImplicitSubtree.prototype.contentIsAvailableAtCoordinates = function (
  implicitCoordinates,
  contentIndex
) {
  const index = this.getTileIndex(implicitCoordinates);
  return this.contentIsAvailableAtIndex(index, contentIndex);
};

/**
 * Check if a child subtree is available at an index of the child subtree availability bitstream
 *
 * @param {number} index The index of the desired child subtree
 * @returns {boolean} The value of the i-th bit
 * @private
 */
ImplicitSubtree.prototype.childSubtreeIsAvailableAtIndex = function (index) {
  return this._childSubtreeAvailability.getBit(index);
};

/**
 * Check if a specific child subtree is available at an implicit tile coordinate
 *
 * @param {ImplicitTileCoordinates} implicitCoordinates The global coordinates of a child subtree
 * @returns {boolean} The value of the i-th bit
 * @private
 */
ImplicitSubtree.prototype.childSubtreeIsAvailableAtCoordinates = function (
  implicitCoordinates
) {
  const index = this.getChildSubtreeIndex(implicitCoordinates);
  return this.childSubtreeIsAvailableAtIndex(index);
};

/**
 * Get the index of the first node at the given level within this subtree.
 * e.g. for a quadtree:
 * <ul>
 * <li>Level 0 starts at index 0</li>
 * <li>Level 1 starts at index 1</li>
 * <li>Level 2 starts at index 5</li>
 * </ul>
 *
 * @param {number} level The 0-indexed level number relative to the root of the subtree
 * @returns {number} The first index at the desired level
 * @private
 */
ImplicitSubtree.prototype.getLevelOffset = function (level) {
  const branchingFactor = this._branchingFactor;
  return (Math.pow(branchingFactor, level) - 1) / (branchingFactor - 1);
};

/**
 * Get the morton index of a tile's parent. This is equivalent to
 * chopping off the last 2 (quadtree) or 3 (octree) bits of the morton
 * index.
 *
 * @param {number} childIndex The morton index of the child tile relative to its parent
 * @returns {number} The index of the child's parent node
 * @private
 */
ImplicitSubtree.prototype.getParentMortonIndex = function (mortonIndex) {
  let bitsPerLevel = 2;
  if (this._subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) {
    bitsPerLevel = 3;
  }

  return mortonIndex >> bitsPerLevel;
};

/**
 * Parse all relevant information out of the subtree. This fetches any
 * external buffers that are used by the implicit tileset.
 *
 * @param {Resource} resource The resource for this subtree. This is used for fetching external buffers as needed.
 * @param {object} [json] The JSON object for this subtree. If parsing from a binary subtree file, this will be undefined.
 * @param {Uint8Array} [subtreeView] The contents of the subtree binary
 * @param {ImplicitTileset} implicitTileset The implicit tileset this subtree belongs to.
 * @param {ImplicitTileCoordinates} implicitCoordinates The coordinates of the subtree's root tile.
 * @return {Promise<ImplicitSubtree>} The created subtree
 * @private
 *
 * @exception {DeveloperError} One of json and subtreeView must be defined.
 */
ImplicitSubtree.fromSubtreeJson = async function (
  resource,
  json,
  subtreeView,
  implicitTileset,
  implicitCoordinates
) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("resource", resource);
  if (defined(json) === defined(subtreeView)) {
    throw new DeveloperError("One of json and subtreeView must be defined.");
  }
  Check.typeOf.object("implicitTileset", implicitTileset);
  Check.typeOf.object("implicitCoordinates", implicitCoordinates);
  //>>includeEnd('debug');

  const subtree = new ImplicitSubtree(
    resource,
    implicitTileset,
    implicitCoordinates
  );

  let chunks;
  if (defined(json)) {
    chunks = {
      json: json,
      binary: undefined,
    };
  } else {
    chunks = parseSubtreeChunks(subtreeView);
  }

  const subtreeJson = chunks.json;
  subtree._subtreeJson = subtreeJson;

  let tilePropertyTableJson;
  if (hasExtension(subtreeJson, "3DTILES_metadata")) {
    tilePropertyTableJson = subtreeJson.extensions["3DTILES_metadata"];
  } else if (defined(subtreeJson.tileMetadata)) {
    const propertyTableIndex = subtreeJson.tileMetadata;
    tilePropertyTableJson = subtreeJson.propertyTables[propertyTableIndex];
  }

  const contentPropertyTableJsons = [];
  if (defined(subtreeJson.contentMetadata)) {
    const length = subtreeJson.contentMetadata.length;
    for (let i = 0; i < length; i++) {
      const propertyTableIndex = subtreeJson.contentMetadata[i];
      contentPropertyTableJsons.push(
        subtreeJson.propertyTables[propertyTableIndex]
      );
    }
  }

  let metadata;
  const schema = implicitTileset.metadataSchema;
  const subtreeMetadata = subtreeJson.subtreeMetadata;
  if (defined(subtreeMetadata)) {
    const metadataClass = subtreeMetadata.class;
    const subtreeMetadataClass = schema.classes[metadataClass];
    metadata = new ImplicitSubtreeMetadata({
      subtreeMetadata: subtreeMetadata,
      class: subtreeMetadataClass,
    });
  }

  subtree._metadata = metadata;
  subtree._tilePropertyTableJson = tilePropertyTableJson;
  subtree._contentPropertyTableJsons = contentPropertyTableJsons;

  // if no contentAvailability is specified, no tile in the subtree has
  // content
  const defaultContentAvailability = {
    constant: 0,
  };

  // In 3D Tiles 1.1, content availability is provided in an array in the subtree JSON
  // regardless of whether or not it contains multiple contents. This differs from previous
  // schemas, where content availability is either a single object in the subtree JSON or
  // as an array in the 3DTILES_multiple_contents extension.
  //
  // After identifying how availability is stored, put the results in this new array for consistent processing later
  subtreeJson.contentAvailabilityHeaders = [];
  if (hasExtension(subtreeJson, "3DTILES_multiple_contents")) {
    subtreeJson.contentAvailabilityHeaders =
      subtreeJson.extensions["3DTILES_multiple_contents"].contentAvailability;
  } else if (Array.isArray(subtreeJson.contentAvailability)) {
    subtreeJson.contentAvailabilityHeaders = subtreeJson.contentAvailability;
  } else {
    subtreeJson.contentAvailabilityHeaders.push(
      defaultValue(subtreeJson.contentAvailability, defaultContentAvailability)
    );
  }

  const bufferHeaders = preprocessBuffers(subtreeJson.buffers);
  const bufferViewHeaders = preprocessBufferViews(
    subtreeJson.bufferViews,
    bufferHeaders
  );

  // Buffers and buffer views are inactive until explicitly marked active.
  // This way we can avoid fetching buffers that will not be used.
  markActiveBufferViews(subtreeJson, bufferViewHeaders);
  if (defined(tilePropertyTableJson)) {
    markActiveMetadataBufferViews(tilePropertyTableJson, bufferViewHeaders);
  }

  for (let i = 0; i < contentPropertyTableJsons.length; i++) {
    const contentPropertyTableJson = contentPropertyTableJsons[i];
    markActiveMetadataBufferViews(contentPropertyTableJson, bufferViewHeaders);
  }

  const buffersU8 = await requestActiveBuffers(
    subtree,
    bufferHeaders,
    chunks.binary
  );
  const bufferViewsU8 = parseActiveBufferViews(bufferViewHeaders, buffersU8);
  parseAvailability(subtree, subtreeJson, implicitTileset, bufferViewsU8);

  if (defined(tilePropertyTableJson)) {
    parseTileMetadataTable(subtree, implicitTileset, bufferViewsU8);
    makeTileJumpBuffer(subtree);
  }

  parseContentMetadataTables(subtree, implicitTileset, bufferViewsU8);
  makeContentJumpBuffers(subtree);

  subtree._ready = true;
  return subtree;
};

/**
 * A helper object for storing the two parts of the subtree binary
 *
 * @typedef {object} SubtreeChunks
 * @property {object} json The json chunk of the subtree
 * @property {Uint8Array} binary The binary chunk of the subtree. This represents the internal buffer.
 * @private
 */

/**
 * Given the binary contents of a subtree, split into JSON and binary chunks
 *
 * @param {Uint8Array} subtreeView The subtree binary
 * @returns {SubtreeChunks} An object containing the JSON and binary chunks.
 * @private
 */
function parseSubtreeChunks(subtreeView) {
  // Parse the header
  const littleEndian = true;
  const subtreeReader = new DataView(
    subtreeView.buffer,
    subtreeView.byteOffset
  );
  // Skip to the chunk lengths
  let byteOffset = 8;

  // Read the bottom 32 bits of the 64-bit byte length. This is ok for now because:
  // 1) not all browsers have native 64-bit operations
  // 2) the data is well under 4GB
  const jsonByteLength = subtreeReader.getUint32(byteOffset, littleEndian);
  byteOffset += 8;
  const binaryByteLength = subtreeReader.getUint32(byteOffset, littleEndian);
  byteOffset += 8;

  const subtreeJson = getJsonFromTypedArray(
    subtreeView,
    byteOffset,
    jsonByteLength
  );
  byteOffset += jsonByteLength;
  const subtreeBinary = subtreeView.subarray(
    byteOffset,
    byteOffset + binaryByteLength
  );

  return {
    json: subtreeJson,
    binary: subtreeBinary,
  };
}

/**
 * A buffer header is the JSON header from the subtree JSON chunk plus
 * a couple extra boolean flags for easy reference.
 *
 * Buffers are assumed inactive until explicitly marked active. This is used
 * to avoid fetching unneeded buffers.
 *
 * @typedef {object} BufferHeader
 * @property {boolean} isExternal True if this is an external buffer
 * @property {boolean} isActive Whether this buffer is currently used.
 * @property {string} [uri] The URI of the buffer (external buffers only)
 * @property {number} byteLength The byte length of the buffer, including any padding contained within.
 * @private
 */

/**
 * Iterate over the list of buffers from the subtree JSON and add the
 * isExternal and isActive fields for easier parsing later. This modifies
 * the objects in place.
 *
 * @param {Object[]} [bufferHeaders=[]] The JSON from subtreeJson.buffers.
 * @returns {BufferHeader[]} The same array of headers with additional fields.
 * @private
 */
function preprocessBuffers(bufferHeaders) {
  bufferHeaders = defined(bufferHeaders) ? bufferHeaders : [];
  for (let i = 0; i < bufferHeaders.length; i++) {
    const bufferHeader = bufferHeaders[i];
    bufferHeader.isExternal = defined(bufferHeader.uri);
    bufferHeader.isActive = false;
  }

  return bufferHeaders;
}

/**
 * A buffer header is the JSON header from the subtree JSON chunk plus
 * the isActive flag and a reference to the header for the underlying buffer
 *
 * @typedef {object} BufferViewHeader
 * @property {BufferHeader} bufferHeader A reference to the header for the underlying buffer
 * @property {boolean} isActive Whether this bufferView is currently used.
 * @property {number} buffer The index of the underlying buffer.
 * @property {number} byteOffset The start byte of the bufferView within the buffer.
 * @property {number} byteLength The length of the bufferView. No padding is included in this length.
 * @private
 */

/**
 * Iterate the list of buffer views from the subtree JSON and add the
 * isActive flag. Also save a reference to the bufferHeader
 *
 * @param {Object[]} [bufferViewHeaders=[]] The JSON from subtree.bufferViews
 * @param {BufferHeader[]} bufferHeaders The preprocessed buffer headers
 * @returns {BufferViewHeader[]} The same array of bufferView headers with additional fields
 * @private
 */
function preprocessBufferViews(bufferViewHeaders, bufferHeaders) {
  bufferViewHeaders = defined(bufferViewHeaders) ? bufferViewHeaders : [];
  for (let i = 0; i < bufferViewHeaders.length; i++) {
    const bufferViewHeader = bufferViewHeaders[i];
    const bufferHeader = bufferHeaders[bufferViewHeader.buffer];
    bufferViewHeader.bufferHeader = bufferHeader;
    bufferViewHeader.isActive = false;
  }
  return bufferViewHeaders;
}

/**
 * Determine which buffer views need to be loaded into memory. This includes:
 *
 * <ul>
 * <li>The tile availability bitstream (if a bitstream is defined)</li>
 * <li>The content availability bitstream(s) (if a bitstream is defined)</li>
 * <li>The child subtree availability bitstream (if a bitstream is defined)</li>
 * </ul>
 *
 * <p>
 * This function modifies the buffer view headers' isActive flags in place.
 * </p>
 *
 * @param {Object[]} subtreeJson The JSON chunk from the subtree
 * @param {BufferViewHeader[]} bufferViewHeaders The preprocessed buffer view headers
 * @private
 */
function markActiveBufferViews(subtreeJson, bufferViewHeaders) {
  let header;
  const tileAvailabilityHeader = subtreeJson.tileAvailability;

  // Check for bitstream first, which is part of the current schema.
  // bufferView is the name of the bitstream from an older schema.
  if (defined(tileAvailabilityHeader.bitstream)) {
    header = bufferViewHeaders[tileAvailabilityHeader.bitstream];
  } else if (defined(tileAvailabilityHeader.bufferView)) {
    header = bufferViewHeaders[tileAvailabilityHeader.bufferView];
  }

  if (defined(header)) {
    header.isActive = true;
    header.bufferHeader.isActive = true;
  }

  const contentAvailabilityHeaders = subtreeJson.contentAvailabilityHeaders;
  for (let i = 0; i < contentAvailabilityHeaders.length; i++) {
    header = undefined;
    if (defined(contentAvailabilityHeaders[i].bitstream)) {
      header = bufferViewHeaders[contentAvailabilityHeaders[i].bitstream];
    } else if (defined(contentAvailabilityHeaders[i].bufferView)) {
      header = bufferViewHeaders[contentAvailabilityHeaders[i].bufferView];
    }

    if (defined(header)) {
      header.isActive = true;
      header.bufferHeader.isActive = true;
    }
  }

  header = undefined;
  const childSubtreeAvailabilityHeader = subtreeJson.childSubtreeAvailability;
  if (defined(childSubtreeAvailabilityHeader.bitstream)) {
    header = bufferViewHeaders[childSubtreeAvailabilityHeader.bitstream];
  } else if (defined(childSubtreeAvailabilityHeader.bufferView)) {
    header = bufferViewHeaders[childSubtreeAvailabilityHeader.bufferView];
  }

  if (defined(header)) {
    header.isActive = true;
    header.bufferHeader.isActive = true;
  }
}

/**
 * For handling metadata, look over the tile and content metadata buffers
 * <p>
 * This always loads all of the metadata immediately. Future iterations may
 * allow filtering this to avoid downloading unneeded buffers.
 * </p>
 *
 * @param {object} propertyTableJson The property table JSON for either a tile or some content
 * @param {BufferViewHeader[]} bufferViewHeaders The preprocessed buffer view headers
 * @private
 */
function markActiveMetadataBufferViews(propertyTableJson, bufferViewHeaders) {
  const properties = propertyTableJson.properties;
  let header;
  for (const key in properties) {
    if (properties.hasOwnProperty(key)) {
      const metadataHeader = properties[key];

      // An older spec used bufferView
      const valuesBufferView = defaultValue(
        metadataHeader.values,
        metadataHeader.bufferView
      );
      header = bufferViewHeaders[valuesBufferView];
      header.isActive = true;
      header.bufferHeader.isActive = true;

      // An older spec used stringOffsetBufferView
      const stringOffsetBufferView = defaultValue(
        metadataHeader.stringOffsets,
        metadataHeader.stringOffsetBufferView
      );
      if (defined(stringOffsetBufferView)) {
        header = bufferViewHeaders[stringOffsetBufferView];
        header.isActive = true;
        header.bufferHeader.isActive = true;
      }

      // an older spec used arrayOffsetBufferView
      const arrayOffsetBufferView = defaultValue(
        metadataHeader.arrayOffsets,
        metadataHeader.arrayOffsetBufferView
      );
      if (defined(arrayOffsetBufferView)) {
        header = bufferViewHeaders[arrayOffsetBufferView];
        header.isActive = true;
        header.bufferHeader.isActive = true;
      }
    }
  }
}

/**
 * Go through the list of buffers and gather all the active ones into a
 * a dictionary. Since external buffers are allowed, this sometimes involves
 * fetching separate binary files. Consequently, this method returns a promise.
 * <p>
 * The results are put into a dictionary object. The keys are indices of
 * buffers, and the values are Uint8Arrays of the contents. Only buffers
 * marked with the isActive flag are fetched.
 * </p>
 * <p>
 * The internal buffer (the subtree's binary chunk) is also stored in this
 * dictionary if it is marked active.
 * </p>
 * @param {ImplicitSubtree} subtree The subtree
 * @param {BufferHeader[]} bufferHeaders The preprocessed buffer headers
 * @param {Uint8Array} internalBuffer The binary chunk of the subtree file
 * @returns {Promise<object>} A promise resolving to the dictionary of active buffers
 * @private
 */
function requestActiveBuffers(subtree, bufferHeaders, internalBuffer) {
  const promises = [];
  for (let i = 0; i < bufferHeaders.length; i++) {
    const bufferHeader = bufferHeaders[i];
    if (!bufferHeader.isActive) {
      promises.push(Promise.resolve(undefined));
    } else if (bufferHeader.isExternal) {
      const promise = requestExternalBuffer(subtree, bufferHeader);
      promises.push(promise);
    } else {
      promises.push(Promise.resolve(internalBuffer));
    }
  }
  return Promise.all(promises).then(function (bufferResults) {
    const buffersU8 = {};
    for (let i = 0; i < bufferResults.length; i++) {
      const result = bufferResults[i];
      if (defined(result)) {
        buffersU8[i] = result;
      }
    }
    return buffersU8;
  });
}

async function requestExternalBuffer(subtree, bufferHeader) {
  const baseResource = subtree._resource;
  const bufferResource = baseResource.getDerivedResource({
    url: bufferHeader.uri,
  });

  const bufferLoader = ResourceCache.getExternalBufferLoader({
    resource: bufferResource,
  });
  subtree._bufferLoader = bufferLoader;

  try {
    await bufferLoader.load();
  } catch (error) {
    if (bufferLoader.isDestroyed()) {
      return;
    }

    throw error;
  }

  return bufferLoader.typedArray;
}

/**
 * Go through the list of buffer views, and if they are marked as active,
 * extract a subarray from one of the active buffers.
 *
 * @param {BufferViewHeader[]} bufferViewHeaders
 * @param {object} buffersU8 A dictionary of buffer index to a Uint8Array of its contents.
 * @returns {object} A dictionary of buffer view index to a Uint8Array of its contents.
 * @private
 */
function parseActiveBufferViews(bufferViewHeaders, buffersU8) {
  const bufferViewsU8 = {};
  for (let i = 0; i < bufferViewHeaders.length; i++) {
    const bufferViewHeader = bufferViewHeaders[i];

    if (!bufferViewHeader.isActive) {
      continue;
    }

    const start = bufferViewHeader.byteOffset;
    const end = start + bufferViewHeader.byteLength;
    const buffer = buffersU8[bufferViewHeader.buffer];
    const bufferView = buffer.subarray(start, end);
    bufferViewsU8[i] = bufferView;
  }
  return bufferViewsU8;
}

/**
 * Parse the three availability bitstreams and store them in the subtree
 *
 * @param {ImplicitSubtree} subtree The subtree to modify
 * @param {object} subtreeJson The subtree JSON
 * @param {ImplicitTileset} implicitTileset The implicit tileset this subtree belongs to
 * @param {object} bufferViewsU8 A dictionary of buffer view index to a Uint8Array of its contents.
 * @private
 */
function parseAvailability(
  subtree,
  subtreeJson,
  implicitTileset,
  bufferViewsU8
) {
  const branchingFactor = implicitTileset.branchingFactor;
  const subtreeLevels = implicitTileset.subtreeLevels;
  const tileAvailabilityBits =
    (Math.pow(branchingFactor, subtreeLevels) - 1) / (branchingFactor - 1);
  const childSubtreeBits = Math.pow(branchingFactor, subtreeLevels);

  // availableCount is only needed for the metadata jump buffer, which
  // corresponds to the tile availability bitstream.
  const hasMetadataExtension = hasExtension(subtreeJson, "3DTILES_metadata");
  const hasTileMetadata = defined(subtree._tilePropertyTableJson);
  let computeAvailableCountEnabled = hasMetadataExtension || hasTileMetadata;

  subtree._tileAvailability = parseAvailabilityBitstream(
    subtreeJson.tileAvailability,
    bufferViewsU8,
    tileAvailabilityBits,
    computeAvailableCountEnabled
  );

  const hasContentMetadata = subtree._contentPropertyTableJsons.length > 0;
  computeAvailableCountEnabled =
    computeAvailableCountEnabled || hasContentMetadata;

  for (let i = 0; i < subtreeJson.contentAvailabilityHeaders.length; i++) {
    const bitstream = parseAvailabilityBitstream(
      subtreeJson.contentAvailabilityHeaders[i],
      bufferViewsU8,
      // content availability has the same length as tile availability.
      tileAvailabilityBits,
      computeAvailableCountEnabled
    );
    subtree._contentAvailabilityBitstreams.push(bitstream);
  }

  subtree._childSubtreeAvailability = parseAvailabilityBitstream(
    subtreeJson.childSubtreeAvailability,
    bufferViewsU8,
    childSubtreeBits
  );
}

/**
 * Given the JSON describing an availability bitstream, turn it into an
 * in-memory representation using an {@link ImplicitAvailabilityBitstream}
 * object. This handles both constants and bitstreams from a bufferView.
 *
 * @param {object} availabilityJson A JSON object representing the availability
 * @param {object} bufferViewsU8 A dictionary of bufferView index to its Uint8Array contents.
 * @param {number} lengthBits The length of the availability bitstream in bits
 * @param {boolean} [computeAvailableCountEnabled] If true and availabilityJson.availableCount is undefined, the availableCount will be computed.
 * @returns {ImplicitAvailabilityBitstream} The parsed bitstream object
 * @private
 */
function parseAvailabilityBitstream(
  availabilityJson,
  bufferViewsU8,
  lengthBits,
  computeAvailableCountEnabled
) {
  if (defined(availabilityJson.constant)) {
    return new ImplicitAvailabilityBitstream({
      constant: Boolean(availabilityJson.constant),
      lengthBits: lengthBits,
      availableCount: availabilityJson.availableCount,
    });
  }

  let bufferView;

  // Check for bitstream first, which is part of the current schema.
  // bufferView is the name of the bitstream from an older schema.
  if (defined(availabilityJson.bitstream)) {
    bufferView = bufferViewsU8[availabilityJson.bitstream];
  } else if (defined(availabilityJson.bufferView)) {
    bufferView = bufferViewsU8[availabilityJson.bufferView];
  }

  return new ImplicitAvailabilityBitstream({
    bitstream: bufferView,
    lengthBits: lengthBits,
    availableCount: availabilityJson.availableCount,
    computeAvailableCountEnabled: computeAvailableCountEnabled,
  });
}

/**
 * Parse the metadata table for the tile metadata, storing a {@link MetadataTable}
 * in the subtree.
 *
 * @param {ImplicitSubtree} subtree The subtree
 * @param {ImplicitTileset} implicitTileset The implicit tileset this subtree belongs to.
 * @param {object} bufferViewsU8 A dictionary of bufferView index to its Uint8Array contents.
 * @private
 */
function parseTileMetadataTable(subtree, implicitTileset, bufferViewsU8) {
  const tilePropertyTableJson = subtree._tilePropertyTableJson;
  const tileCount = subtree._tileAvailability.availableCount;
  const metadataSchema = implicitTileset.metadataSchema;

  const tileMetadataClassName = tilePropertyTableJson.class;
  const tileMetadataClass = metadataSchema.classes[tileMetadataClassName];

  subtree._tileMetadataTable = new MetadataTable({
    class: tileMetadataClass,
    count: tileCount,
    properties: tilePropertyTableJson.properties,
    bufferViews: bufferViewsU8,
  });
}

/**
 * Parse the metadata tables for the content metadata, storing an array of
 * {@link MetadataTable}s in the subtree.
 *
 * @param {ImplicitSubtree} subtree The subtree
 * @param {ImplicitTileset} implicitTileset The implicit tileset this subtree belongs to.
 * @param {object} bufferViewsU8 A dictionary of bufferView index to its Uint8Array contents.
 * @private
 */
function parseContentMetadataTables(subtree, implicitTileset, bufferViewsU8) {
  const contentPropertyTableJsons = subtree._contentPropertyTableJsons;
  const contentAvailabilityBitstreams = subtree._contentAvailabilityBitstreams;
  const metadataSchema = implicitTileset.metadataSchema;

  const contentMetadataTables = subtree._contentMetadataTables;
  for (let i = 0; i < contentPropertyTableJsons.length; i++) {
    const contentPropertyTableJson = contentPropertyTableJsons[i];
    const contentAvailabilityBitsteam = contentAvailabilityBitstreams[i];

    const contentCount = contentAvailabilityBitsteam.availableCount;
    const contentMetadataClassName = contentPropertyTableJson.class;
    const contentMetadataClass =
      metadataSchema.classes[contentMetadataClassName];

    const metadataTable = new MetadataTable({
      class: contentMetadataClass,
      count: contentCount,
      properties: contentPropertyTableJson.properties,
      bufferViews: bufferViewsU8,
    });

    contentMetadataTables.push(metadataTable);
  }
}

/**
 * Make a jump buffer, i.e. a map of a bit index to the metadata entity ID.
 * <p>
 * For unavailable tiles and content, the jump buffer entries will be uninitialized.
 * Use the tile and content availability to determine whether a jump buffer value is valid.
 * </p>
 *
 * @param {ImplicitAvailabilityBitstream} availability The availability bitstream to create the jump buffer from.
 * @returns {Array} The resulting jump buffer.
 * @private
 */
function makeJumpBuffer(availability) {
  let entityId = 0;
  const bufferLength = availability.lengthBits;
  const availableCount = availability.availableCount;

  let jumpBuffer;
  if (availableCount < 256) {
    jumpBuffer = new Uint8Array(bufferLength);
  } else if (availableCount < 65536) {
    jumpBuffer = new Uint16Array(bufferLength);
  } else {
    jumpBuffer = new Uint32Array(bufferLength);
  }

  for (let i = 0; i < availability.lengthBits; i++) {
    if (availability.getBit(i)) {
      jumpBuffer[i] = entityId;
      entityId++;
    }
  }

  return jumpBuffer;
}

/**
 * Make the jump buffer, i.e. a map of a bit index to the metadata entity ID,
 * for the content metadata. This is stored in the subtree.
 *
 * @param {ImplicitSubtree} subtree The subtree
 * @private
 */
function makeTileJumpBuffer(subtree) {
  const tileJumpBuffer = makeJumpBuffer(subtree._tileAvailability);
  subtree._tileJumpBuffer = tileJumpBuffer;
}

/**
 * Make the jump buffers, i.e. maps of bit indices to the metadata entity IDs,
 * for the content metadata. This is stored in the subtree.
 *
 * @param {ImplicitSubtree} subtree The subtree
 * @private
 */
function makeContentJumpBuffers(subtree) {
  const contentJumpBuffers = subtree._contentJumpBuffers;
  const contentAvailabilityBitstreams = subtree._contentAvailabilityBitstreams;
  for (let i = 0; i < contentAvailabilityBitstreams.length; i++) {
    const contentAvailability = contentAvailabilityBitstreams[i];
    const contentJumpBuffer = makeJumpBuffer(contentAvailability);
    contentJumpBuffers.push(contentJumpBuffer);
  }
}

/**
 * Given the implicit tiling coordinates for a tile, get the index within the
 * subtree's tile availability bitstream.
 * @param {ImplicitTileCoordinates} implicitCoordinates The global coordinates of a tile
 * @return {number} The tile's index within the subtree.
 * @private
 */
ImplicitSubtree.prototype.getTileIndex = function (implicitCoordinates) {
  const localLevel =
    implicitCoordinates.level - this._implicitCoordinates.level;
  if (localLevel < 0 || this._subtreeLevels <= localLevel) {
    throw new RuntimeError("level is out of bounds for this subtree");
  }

  const subtreeCoordinates = implicitCoordinates.getSubtreeCoordinates();
  const offsetCoordinates = subtreeCoordinates.getOffsetCoordinates(
    implicitCoordinates
  );
  const index = offsetCoordinates.tileIndex;
  return index;
};

/**
 * Given the implicit tiling coordinates for a child subtree, get the index within the
 * subtree's child subtree availability bitstream.
 * @param {ImplicitTileCoordinates} implicitCoordinates The global coordinates of a child subtree
 * @return {number} The child subtree's index within the subtree's child subtree availability bitstream.
 * @private
 */
ImplicitSubtree.prototype.getChildSubtreeIndex = function (
  implicitCoordinates
) {
  const localLevel =
    implicitCoordinates.level - this._implicitCoordinates.level;
  if (localLevel !== this._implicitCoordinates.subtreeLevels) {
    throw new RuntimeError("level is out of bounds for this subtree");
  }

  // Call getParentSubtreeCoordinates instead of getSubtreeCoordinates because the
  // child subtree is by definition the root of its own subtree, so we need to find
  // the parent subtree.
  const parentSubtreeCoordinates = implicitCoordinates.getParentSubtreeCoordinates();
  const offsetCoordinates = parentSubtreeCoordinates.getOffsetCoordinates(
    implicitCoordinates
  );
  const index = offsetCoordinates.mortonIndex;
  return index;
};

/**
 * Get the entity ID for a tile within this subtree.
 * @param {ImplicitSubtree} subtree The subtree
 * @param {ImplicitTileCoordinates} implicitCoordinates The global coordinates of a tile
 * @return {number} The entity ID for this tile for accessing tile metadata, or <code>undefined</code> if not applicable.
 *
 * @private
 */
function getTileEntityId(subtree, implicitCoordinates) {
  if (!defined(subtree._tileMetadataTable)) {
    return undefined;
  }

  const tileIndex = subtree.getTileIndex(implicitCoordinates);
  if (subtree._tileAvailability.getBit(tileIndex)) {
    return subtree._tileJumpBuffer[tileIndex];
  }

  return undefined;
}

/**
 * Get the entity ID for a content within this subtree.
 * @param {ImplicitSubtree} subtree The subtree
 * @param {ImplicitTileCoordinates} implicitCoordinates The global coordinates of a content
 * @param {number} contentIndex The content index, for distinguishing between multiple contents.
 * @return {number} The entity ID for this content for accessing content metadata, or <code>undefined</code> if not applicable.
 *
 * @private
 */
function getContentEntityId(subtree, implicitCoordinates, contentIndex) {
  const metadataTables = subtree._contentMetadataTables;
  if (!defined(metadataTables)) {
    return undefined;
  }

  const metadataTable = metadataTables[contentIndex];
  if (!defined(metadataTable)) {
    return undefined;
  }

  const availability = subtree._contentAvailabilityBitstreams[contentIndex];
  const tileIndex = subtree.getTileIndex(implicitCoordinates);
  if (availability.getBit(tileIndex)) {
    const contentJumpBuffer = subtree._contentJumpBuffers[contentIndex];
    return contentJumpBuffer[tileIndex];
  }

  return undefined;
}

/**
 * Create and return a metadata table view for a tile within this subtree.
 * @param {ImplicitTileCoordinates} implicitCoordinates The global coordinates of a tile
 * @return {ImplicitMetadataView} The metadata view for this tile, or <code>undefined</code> if not applicable.
 *
 * @private
 */
ImplicitSubtree.prototype.getTileMetadataView = function (implicitCoordinates) {
  const entityId = getTileEntityId(this, implicitCoordinates);
  if (!defined(entityId)) {
    return undefined;
  }

  const metadataTable = this._tileMetadataTable;
  return new ImplicitMetadataView({
    class: metadataTable.class,
    metadataTable: metadataTable,
    entityId: entityId,
    propertyTableJson: this._tilePropertyTableJson,
  });
};

/**
 * Create and return a metadata table view for a content within this subtree.
 * @param {ImplicitTileCoordinates} implicitCoordinates The global coordinates of a content
 * @param {number} contentIndex The index of the content used to distinguish between multiple contents
 * @return {ImplicitMetadataView} The metadata view for this content, or <code>undefined</code> if not applicable.
 *
 * @private
 */
ImplicitSubtree.prototype.getContentMetadataView = function (
  implicitCoordinates,
  contentIndex
) {
  const entityId = getContentEntityId(this, implicitCoordinates, contentIndex);
  if (!defined(entityId)) {
    return undefined;
  }

  const metadataTable = this._contentMetadataTables[contentIndex];
  const propertyTableJson = this._contentPropertyTableJsons[contentIndex];
  return new ImplicitMetadataView({
    class: metadataTable.class,
    metadataTable: metadataTable,
    entityId: entityId,
    contentIndex: contentIndex,
    propertyTableJson: propertyTableJson,
  });
};

/**
 * @private
 */
ImplicitSubtree.prototype.isDestroyed = function () {
  return false;
};

/**
 * @private
 */
ImplicitSubtree.prototype.destroy = function () {
  if (defined(this._bufferLoader)) {
    ResourceCache.unload(this._bufferLoader);
  }

  return destroyObject(this);
};

export default ImplicitSubtree;
