import defer from "../Core/defer.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import Request from "../Core/Request.js";
import RequestScheduler from "../Core/RequestScheduler.js";
import RequestState from "../Core/RequestState.js";
import RequestType from "../Core/RequestType.js";
import RuntimeError from "../Core/RuntimeError.js";
import Cesium3DContentGroup from "./Cesium3DContentGroup.js";
import Cesium3DTileContentType from "./Cesium3DTileContentType.js";
import Cesium3DTileContentFactory from "./Cesium3DTileContentFactory.js";
import findContentMetadata from "./findContentMetadata.js";
import findGroupMetadata from "./findGroupMetadata.js";
import preprocess3DTileContent from "./preprocess3DTileContent.js";
/**
 * A collection of contents for tiles that have multiple contents, either via the tile JSON (3D Tiles 1.1) or the 3DTILES_multiple_contents extension.
 * 
* Implements the {@link Cesium3DTileContent} interface. *
* * @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_multiple_contents|3DTILES_multiple_contents extension} * * @alias Multiple3DTileContent * @constructor * * @param {Cesium3DTileset} tileset The tileset this content belongs to * @param {Cesium3DTile} tile The content this content belongs to * @param {Resource} tilesetResource The resource that points to the tileset. This will be used to derive each inner content's resource. * @param {Object} contentsJson Either the tile JSON containing the contents array (3D Tiles 1.1), or3DTILES_multiple_contents extension JSON
 *
 * @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.
 */
export default function Multiple3DTileContent(
  tileset,
  tile,
  tilesetResource,
  contentsJson
) {
  this._tileset = tileset;
  this._tile = tile;
  this._tilesetResource = tilesetResource;
  this._contents = [];
  // An older version of 3DTILES_multiple_contents used "content" instead of "contents"
  const contentHeaders = defined(contentsJson.contents)
    ? contentsJson.contents
    : contentsJson.content;
  this._innerContentHeaders = contentHeaders;
  this._requestsInFlight = 0;
  // How many times cancelPendingRequests() has been called. This is
  // used to help short-circuit computations after a tile was canceled.
  this._cancelCount = 0;
  const contentCount = this._innerContentHeaders.length;
  this._arrayFetchPromises = new Array(contentCount);
  this._requests = new Array(contentCount);
  this._innerContentResources = new Array(contentCount);
  this._serverKeys = new Array(contentCount);
  for (let i = 0; i < contentCount; i++) {
    const contentResource = tilesetResource.getDerivedResource({
      url: contentHeaders[i].uri,
    });
    const serverKey = RequestScheduler.getServerKey(
      contentResource.getUrlComponent()
    );
    this._innerContentResources[i] = contentResource;
    this._serverKeys[i] = serverKey;
  }
  // undefined until the first time requests are scheduled
  this._contentsFetchedPromise = undefined;
  this._readyPromise = defer();
}
Object.defineProperties(Multiple3DTileContent.prototype, {
  /**
   * Part of the {@link Cesium3DTileContent} interface.  Multiple3DTileContent checks if any of the inner contents have dirty featurePropertiesDirty.
   * @memberof Multiple3DTileContent.prototype
   *
   * @type {Boolean}
   *
   * @private
   */
  featurePropertiesDirty: {
    get: function () {
      const contents = this._contents;
      const length = contents.length;
      for (let i = 0; i < length; ++i) {
        if (contents[i].featurePropertiesDirty) {
          return true;
        }
      }
      return false;
    },
    set: function (value) {
      const contents = this._contents;
      const length = contents.length;
      for (let i = 0; i < length; ++i) {
        contents[i].featurePropertiesDirty = value;
      }
    },
  },
  /**
   * Part of the {@link Cesium3DTileContent} interface.  Multiple3DTileContent
   * always returns 0.  Instead call featuresLength for a specific inner content.
   * @memberof Multiple3DTileContent.prototype
   * @private
   */
  featuresLength: {
    get: function () {
      return 0;
    },
  },
  /**
   * Part of the {@link Cesium3DTileContent} interface.  Multiple3DTileContent
   * always returns 0.  Instead, call pointsLength for a specific inner content.
   * @memberof Multiple3DTileContent.prototype
   * @private
   */
  pointsLength: {
    get: function () {
      return 0;
    },
  },
  /**
   * Part of the {@link Cesium3DTileContent} interface.  Multiple3DTileContent
   * always returns 0.  Instead call trianglesLength for a specific inner content.
   * @memberof Multiple3DTileContent.prototype
   * @private
   */
  trianglesLength: {
    get: function () {
      return 0;
    },
  },
  /**
   * Part of the {@link Cesium3DTileContent} interface.  Multiple3DTileContent
   * always returns 0.  Instead call geometryByteLength for a specific inner content.
   * @memberof Multiple3DTileContent.prototype
   * @private
   */
  geometryByteLength: {
    get: function () {
      return 0;
    },
  },
  /**
   * Part of the {@link Cesium3DTileContent} interface.   Multiple3DTileContent
   * always returns 0.  Instead call texturesByteLength for a specific inner content.
   * @memberof Multiple3DTileContent.prototype
   * @private
   */
  texturesByteLength: {
    get: function () {
      return 0;
    },
  },
  /**
   * Part of the {@link Cesium3DTileContent} interface.  Multiple3DTileContent
   * always returns 0.  Instead call batchTableByteLength for a specific inner content.
   * @memberof Multiple3DTileContent.prototype
   * @private
   */
  batchTableByteLength: {
    get: function () {
      return 0;
    },
  },
  innerContents: {
    get: function () {
      return this._contents;
    },
  },
  readyPromise: {
    get: function () {
      return this._readyPromise.promise;
    },
  },
  tileset: {
    get: function () {
      return this._tileset;
    },
  },
  tile: {
    get: function () {
      return this._tile;
    },
  },
  /**
   * Part of the {@link Cesium3DTileContent} interface.
   * Unlike other content types, Multiple3DTileContent does not
   * have a single URL, so this returns undefined.
   * @memberof Multiple3DTileContent.prototype
   *
   * @type {String}
   * @readonly
   * @private
   */
  url: {
    get: function () {
      return undefined;
    },
  },
  /**
   * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent
   * always returns undefined.  Instead call metadata for a specific inner content.
   * @memberof Multiple3DTileContent.prototype
   * @private
   */
  metadata: {
    get: function () {
      return undefined;
    },
    set: function () {
      //>>includeStart('debug', pragmas.debug);
      throw new DeveloperError("Multiple3DTileContent cannot have metadata");
      //>>includeEnd('debug');
    },
  },
  /**
   * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent
   * always returns undefined.  Instead call batchTable for a specific inner content.
   * @memberof Multiple3DTileContent.prototype
   * @private
   */
  batchTable: {
    get: function () {
      return undefined;
    },
  },
  /**
   * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent
   * always returns undefined.  Instead call group for a specific inner content.
   * @memberof Multiple3DTileContent.prototype
   * @private
   */
  group: {
    get: function () {
      return undefined;
    },
    set: function () {
      //>>includeStart('debug', pragmas.debug);
      throw new DeveloperError(
        "Multiple3DTileContent cannot have group metadata"
      );
      //>>includeEnd('debug');
    },
  },
  /**
   * Get an array of the inner content URLs, regardless of whether they've
   * been fetched or not. This is intended for use with
   * {@link Cesium3DTileset#debugShowUrl}.
   * @memberof Multiple3DTileContent.prototype
   *
   * @type {String[]}
   * @readonly
   * @private
   */
  innerContentUrls: {
    get: function () {
      return this._innerContentHeaders.map(function (contentHeader) {
        return contentHeader.uri;
      });
    },
  },
  /**
   * A promise that resolves when all of the inner contents have been fetched.
   * This promise is undefined until the first frame where all array buffer
   * requests have been scheduled.
   * @memberof Multiple3DTileContent.prototype
   *
   * @type {Promise}
   * @private
   */
  contentsFetchedPromise: {
    get: function () {
      if (defined(this._contentsFetchedPromise)) {
        return this._contentsFetchedPromise.promise;
      }
      return undefined;
    },
  },
});
function updatePendingRequests(multipleContents, deltaRequestCount) {
  multipleContents._requestsInFlight += deltaRequestCount;
  multipleContents.tileset.statistics.numberOfPendingRequests += deltaRequestCount;
}
function cancelPendingRequests(multipleContents, originalContentState) {
  multipleContents._cancelCount++;
  // reset the tile's content state to try again later.
  multipleContents._tile._contentState = originalContentState;
  multipleContents.tileset.statistics.numberOfPendingRequests -=
    multipleContents._requestsInFlight;
  multipleContents._requestsInFlight = 0;
  // Discard the request promises.
  const contentCount = multipleContents._innerContentHeaders.length;
  multipleContents._arrayFetchPromises = new Array(contentCount);
}
/**
 * Request the inner contents of this Multiple3DTileContent. This must be called once a frame until
 * {@link Multiple3DTileContent#contentsFetchedPromise} is defined. This promise
 * becomes available as soon as all requests are scheduled.
 * * This method also updates the tile statistics' pending request count if the * requests are successfully scheduled. *
* * @return {Number} The number of attempted requests that were unable to be scheduled. * @private */ Multiple3DTileContent.prototype.requestInnerContents = function () { // It's possible for these promises to leak content array buffers if the // camera moves before they all are scheduled. To prevent this leak, check // if we can schedule all the requests at once. If not, no requests are // scheduled if (!canScheduleAllRequests(this._serverKeys)) { return this._serverKeys.length; } const contentHeaders = this._innerContentHeaders; updatePendingRequests(this, contentHeaders.length); for (let i = 0; i < contentHeaders.length; i++) { // The cancel count is needed to avoid a race condition where a content // is canceled multiple times. this._arrayFetchPromises[i] = requestInnerContent( this, i, this._cancelCount, this._tile._contentState ); } // set up the deferred promise the first time requestInnerContent() // is called. if (!defined(this._contentsFetchedPromise)) { this._contentsFetchedPromise = defer(); } createInnerContents(this); return 0; }; /** * Check if all requests for inner contents can be scheduled at once. This is slower, but it avoids a potential memory leak. * @param {String[]} serverKeys The server keys for all of the inner contents * @return {Boolean} True if the request scheduler has enough open slots for all inner contents * @private */ function canScheduleAllRequests(serverKeys) { const requestCountsByServer = {}; for (let i = 0; i < serverKeys.length; i++) { const serverKey = serverKeys[i]; if (defined(requestCountsByServer[serverKey])) { requestCountsByServer[serverKey]++; } else { requestCountsByServer[serverKey] = 1; } } for (const key in requestCountsByServer) { if ( requestCountsByServer.hasOwnProperty(key) && !RequestScheduler.serverHasOpenSlots(key, requestCountsByServer[key]) ) { return false; } } return RequestScheduler.heapHasOpenSlots(serverKeys.length); } function requestInnerContent( multipleContents, index, originalCancelCount, originalContentState ) { // it is important to clone here. The fetchArrayBuffer() below here uses // throttling, but other uses of the resources do not. const contentResource = multipleContents._innerContentResources[ index ].clone(); const tile = multipleContents.tile; // Always create a new request. If the tile gets canceled, this // avoids getting stuck in the canceled state. const priorityFunction = function () { return tile._priority; }; const serverKey = multipleContents._serverKeys[index]; const request = new Request({ throttle: true, throttleByServer: true, type: RequestType.TILES3D, priorityFunction: priorityFunction, serverKey: serverKey, }); contentResource.request = request; multipleContents._requests[index] = request; return contentResource .fetchArrayBuffer() .then(function (arrayBuffer) { // Short circuit if another inner content was canceled. if (originalCancelCount < multipleContents._cancelCount) { return undefined; } updatePendingRequests(multipleContents, -1); return arrayBuffer; }) .catch(function (error) { // Short circuit if another inner content was canceled. if (originalCancelCount < multipleContents._cancelCount) { return undefined; } if (contentResource.request.state === RequestState.CANCELLED) { cancelPendingRequests(multipleContents, originalContentState); return undefined; } updatePendingRequests(multipleContents, -1); handleInnerContentFailed(multipleContents, index, error); return undefined; }); } function createInnerContents(multipleContents) { const originalCancelCount = multipleContents._cancelCount; Promise.all(multipleContents._arrayFetchPromises) .then(function (arrayBuffers) { if (originalCancelCount < multipleContents._cancelCount) { return undefined; } return arrayBuffers.map(function (arrayBuffer, i) { if (!defined(arrayBuffer)) { // Content was not fetched. The error was handled in // the fetch promise return undefined; } try { return createInnerContent(multipleContents, arrayBuffer, i); } catch (error) { handleInnerContentFailed(multipleContents, i, error); return undefined; } }); }) .then(function (contents) { if (!defined(contents)) { // request was canceled. resolve the promise (Cesium3DTile will // detect that the the content was canceled), then discard the promise // so a new one can be created if (defined(multipleContents._contentsFetchedPromise)) { multipleContents._contentsFetchedPromise.resolve(); multipleContents._contentsFetchedPromise = undefined; } return; } multipleContents._contents = contents.filter(defined); awaitReadyPromises(multipleContents); if (defined(multipleContents._contentsFetchedPromise)) { multipleContents._contentsFetchedPromise.resolve(); } }) .catch(function (error) { if (defined(multipleContents._contentsFetchedPromise)) { multipleContents._contentsFetchedPromise.reject(error); } }); } function createInnerContent(multipleContents, arrayBuffer, index) { const preprocessed = preprocess3DTileContent(arrayBuffer); if (preprocessed.contentType === Cesium3DTileContentType.EXTERNAL_TILESET) { throw new RuntimeError( "External tilesets are disallowed inside multiple contents" ); } multipleContents._disableSkipLevelOfDetail = multipleContents._disableSkipLevelOfDetail || preprocessed.contentType === Cesium3DTileContentType.GEOMETRY || preprocessed.contentType === Cesium3DTileContentType.VECTOR; const tileset = multipleContents._tileset; const resource = multipleContents._innerContentResources[index]; const tile = multipleContents._tile; let content; const contentFactory = Cesium3DTileContentFactory[preprocessed.contentType]; if (defined(preprocessed.binaryPayload)) { content = contentFactory( tileset, tile, resource, preprocessed.binaryPayload.buffer, 0 ); } else { // JSON formats content = contentFactory(tileset, tile, resource, preprocessed.jsonPayload); } const contentHeader = multipleContents._innerContentHeaders[index]; if (tile.hasImplicitContentMetadata) { const subtree = tile.implicitSubtree; const coordinates = tile.implicitCoordinates; content.metadata = subtree.getContentMetadataView(coordinates, index); } else if (!tile.hasImplicitContent) { content.metadata = findContentMetadata(tileset, contentHeader); } const groupMetadata = findGroupMetadata(tileset, contentHeader); if (defined(groupMetadata)) { content.group = new Cesium3DContentGroup({ metadata: groupMetadata, }); } return content; } function awaitReadyPromises(multipleContents) { const readyPromises = multipleContents._contents.map(function (content) { return content.readyPromise; }); Promise.all(readyPromises) .then(function () { multipleContents._readyPromise.resolve(multipleContents); }) .catch(function (error) { multipleContents._readyPromise.reject(error); }); } function handleInnerContentFailed(multipleContents, index, error) { const tileset = multipleContents._tileset; const url = multipleContents._innerContentResources[index].url; const message = defined(error.message) ? error.message : error.toString(); if (tileset.tileFailed.numberOfListeners > 0) { tileset.tileFailed.raiseEvent({ url: url, message: message, }); } else { console.log(`A content failed to load: ${url}`); console.log(`Error: ${message}`); } } /** * Cancel all requests for inner contents. This is called by the tile * when a tile goes out of view. * * @private */ Multiple3DTileContent.prototype.cancelRequests = function () { for (let i = 0; i < this._requests.length; i++) { const request = this._requests[i]; if (defined(request)) { request.cancel(); } } }; /** * Part of the {@link Cesium3DTileContent} interface.Multiple3DTileContent
 * always returns false.  Instead call hasProperty for a specific inner content
 * @private
 */
Multiple3DTileContent.prototype.hasProperty = function (batchId, name) {
  return false;
};
/**
 * Part of the {@link Cesium3DTileContent} interface.  Multiple3DTileContent
 * always returns undefined.  Instead call getFeature for a specific inner content
 * @private
 */
Multiple3DTileContent.prototype.getFeature = function (batchId) {
  return undefined;
};
Multiple3DTileContent.prototype.applyDebugSettings = function (enabled, color) {
  const contents = this._contents;
  const length = contents.length;
  for (let i = 0; i < length; ++i) {
    contents[i].applyDebugSettings(enabled, color);
  }
};
Multiple3DTileContent.prototype.applyStyle = function (style) {
  const contents = this._contents;
  const length = contents.length;
  for (let i = 0; i < length; ++i) {
    contents[i].applyStyle(style);
  }
};
Multiple3DTileContent.prototype.update = function (tileset, frameState) {
  const contents = this._contents;
  const length = contents.length;
  for (let i = 0; i < length; ++i) {
    contents[i].update(tileset, frameState);
  }
};
Multiple3DTileContent.prototype.isDestroyed = function () {
  return false;
};
Multiple3DTileContent.prototype.destroy = function () {
  const contents = this._contents;
  const length = contents.length;
  for (let i = 0; i < length; ++i) {
    contents[i].destroy();
  }
  return destroyObject(this);
};