import Cesium3DTileset from "./Cesium3DTileset.js";
import defined from "../Core/defined.js";
import Resource from "../Core/Resource.js";
import ITwinPlatform from "../Core/ITwinPlatform.js";
import RuntimeError from "../Core/RuntimeError.js";
import Check from "../Core/Check.js";
import KmlDataSource from "../DataSources/KmlDataSource.js";
import GeoJsonDataSource from "../DataSources/GeoJsonDataSource.js";
import DeveloperError from "../Core/DeveloperError.js";

/**
 * Methods for loading iTwin platform data into CesiumJS
 *
 * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
 *
 * @see ITwinPlatform
 * @namespace ITwinData
 */
const ITwinData = {};

/**
 * Create a {@link Cesium3DTileset} for the given iModel id using iTwin's Mesh Export API.
 *
 * If there is not a completed export available for the given iModel id, the returned promise will resolve to <code>undefined</code>.
 * We recommend waiting 10-20 seconds and trying to load the tileset again.
 * If all exports are Invalid this will throw an error.
 *
 * See the {@link https://developer.bentley.com/apis/mesh-export/overview/|iTwin Platform Mesh Export API documentation} for more information on request parameters
 *
 * @example
 * const tileset = await Cesium.ITwinData.createTilesetFromIModelId({ iModelId });
 * if (Cesium.defined(tileset)) {
 *   viewer.scene.primitives.add(tileset);
 * }
 *
 * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
 *
 * @param {object} options
 * @param {string} options.iModelId The id of the iModel to load
 * @param {Cesium3DTileset.ConstructorOptions} [options.tilesetOptions] Object containing options to pass to the internally created {@link Cesium3DTileset}.
 * @param {string} [options.changesetId] The id of the changeset to load, if not provided the latest changesets will be used
 * @returns {Promise<Cesium3DTileset | undefined>} A promise that will resolve to the created 3D tileset or <code>undefined</code> if there is no completed export for the given iModel id
 *
 * @throws {RuntimeError} If all exports for the given iModel are Invalid
 * @throws {RuntimeError} If the iTwin API request is not successful
 */
ITwinData.createTilesetFromIModelId = async function ({
  iModelId,
  changesetId,
  tilesetOptions,
}) {
  const { exports } = await ITwinPlatform.getExports(iModelId, changesetId);

  if (
    exports.length > 0 &&
    exports.every((exportObj) => {
      return exportObj.status === ITwinPlatform.ExportStatus.Invalid;
    })
  ) {
    throw new RuntimeError(
      `All exports for this iModel are Invalid: ${iModelId}`,
    );
  }

  const completeExport = exports.find((exportObj) => {
    return exportObj.status === ITwinPlatform.ExportStatus.Complete;
  });

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

  // Convert the link to the tileset url while preserving the search paramaters
  // This link is only valid 1 hour
  const baseUrl = new URL(completeExport._links.mesh.href);
  baseUrl.pathname = `${baseUrl.pathname}/tileset.json`;
  const tilesetUrl = baseUrl.toString();

  const resource = new Resource({
    url: tilesetUrl,
  });

  return Cesium3DTileset.fromUrl(resource, tilesetOptions);
};

/**
 * Create a tileset for the specified reality data id. This function only works
 * with 3D Tiles meshes and point clouds.
 *
 * If the <code>type</code> or <code>rootDocument</code> are not provided this function
 * will first request the full metadata for the specified reality data to fill these values.
 *
 * The <code>maximumScreenSpaceError</code> of the resulting tileset will default to 4,
 * unless it is explicitly overridden with the given tileset options.
 *
 * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
 *
 * @param {object} options
 * @param {string} options.iTwinId The id of the iTwin to load data from
 * @param {string} options.realityDataId The id of the reality data to load
 * @param {ITwinPlatform.RealityDataType} [options.type] The type of this reality data
 * @param {string} [options.rootDocument] The path of the root document for this reality data
 * @param {Cesium3DTileset.ConstructorOptions} [options.tilesetOptions] Object containing
 * options to pass to the internally created {@link Cesium3DTileset}.
 * @returns {Promise<Cesium3DTileset>}
 *
 * @throws {RuntimeError} if the type of reality data is not supported by this function
 */
ITwinData.createTilesetForRealityDataId = async function ({
  iTwinId,
  realityDataId,
  type,
  rootDocument,
  tilesetOptions,
}) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.string("iTwinId", iTwinId);
  Check.typeOf.string("realityDataId", realityDataId);
  if (defined(type)) {
    Check.typeOf.string("type", type);
  }
  if (defined(rootDocument)) {
    Check.typeOf.string("rootDocument", rootDocument);
  }
  //>>includeEnd('debug');

  if (!defined(type) || !defined(rootDocument)) {
    const metadata = await ITwinPlatform.getRealityDataMetadata(
      iTwinId,
      realityDataId,
    );
    rootDocument = metadata.rootDocument;
    type = metadata.type;
  }

  const supportedRealityDataTypes = [
    ITwinPlatform.RealityDataType.Cesium3DTiles,
    ITwinPlatform.RealityDataType.PNTS,
    ITwinPlatform.RealityDataType.RealityMesh3DTiles,
    ITwinPlatform.RealityDataType.Terrain3DTiles,
  ];

  if (!supportedRealityDataTypes.includes(type)) {
    throw new RuntimeError(`Reality data type is not a mesh type: ${type}`);
  }

  const tilesetAccessUrl = await ITwinPlatform.getRealityDataURL(
    iTwinId,
    realityDataId,
    rootDocument,
  );

  // The maximum screen space error was defined to default to 4 for
  // reality data tilesets, because they did not show the expected
  // amount of detail with the default value of 16. Values that are
  // given in the tilesetOptions should still override that default.
  const internalTilesetOptions = {
    maximumScreenSpaceError: 4,
    ...tilesetOptions,
  };

  return Cesium3DTileset.fromUrl(tilesetAccessUrl, internalTilesetOptions);
};

/**
 * Create a data source of the correct type for the specified reality data id.
 * This function only works for KML and GeoJSON type data.
 *
 * If the <code>type</code> or <code>rootDocument</code> are not provided this function
 * will first request the full metadata for the specified reality data to fill these values.
 *
 * @param {object} options
 * @param {string} options.iTwinId The id of the iTwin to load data from
 * @param {string} options.realityDataId The id of the reality data to load
 * @param {ITwinPlatform.RealityDataType} [options.type] The type of this reality data
 * @param {string} [options.rootDocument] The path of the root document for this reality data
 * @returns {Promise<GeoJsonDataSource | KmlDataSource>}
 *
 * @throws {RuntimeError} if the type of reality data is not supported by this function
 */
ITwinData.createDataSourceForRealityDataId = async function ({
  iTwinId,
  realityDataId,
  type,
  rootDocument,
}) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.string("iTwinId", iTwinId);
  Check.typeOf.string("realityDataId", realityDataId);
  if (defined(type)) {
    Check.typeOf.string("type", type);
  }
  if (defined(rootDocument)) {
    Check.typeOf.string("rootDocument", rootDocument);
  }
  //>>includeEnd('debug');

  if (!defined(type) || !defined(rootDocument)) {
    const metadata = await ITwinPlatform.getRealityDataMetadata(
      iTwinId,
      realityDataId,
    );
    rootDocument = metadata.rootDocument;
    type = metadata.type;
  }

  const supportedRealityDataTypes = [
    ITwinPlatform.RealityDataType.KML,
    ITwinPlatform.RealityDataType.GeoJSON,
  ];

  if (!supportedRealityDataTypes.includes(type)) {
    throw new RuntimeError(
      `Reality data type is not a data source type: ${type}`,
    );
  }

  const tilesetAccessUrl = await ITwinPlatform.getRealityDataURL(
    iTwinId,
    realityDataId,
    rootDocument,
  );

  if (type === ITwinPlatform.RealityDataType.GeoJSON) {
    return GeoJsonDataSource.load(tilesetAccessUrl);
  }

  // If we get here it's guaranteed to be a KML type
  return KmlDataSource.load(tilesetAccessUrl);
};

/**
 * Load data from the Geospatial Features API as GeoJSON.
 *
 * @param {object} options
 * @param {string} options.iTwinId The id of the iTwin to load data from
 * @param {string} options.collectionId The id of the data collection to load
 * @param {number} [options.limit=10000] number of items per page, must be between 1 and 10,000 inclusive
 * @returns {Promise<GeoJsonDataSource>}
 */
ITwinData.loadGeospatialFeatures = async function ({
  iTwinId,
  collectionId,
  limit,
}) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.string("iTwinId", iTwinId);
  Check.typeOf.string("collectionId", collectionId);
  if (defined(limit)) {
    Check.typeOf.number("limit", limit);
    Check.typeOf.number.lessThanOrEquals("limit", limit, 10000);
    Check.typeOf.number.greaterThanOrEquals("limit", limit, 1);
  }
  if (
    !defined(ITwinPlatform.defaultAccessToken) &&
    !defined(ITwinPlatform.defaultShareKey)
  ) {
    throw new DeveloperError(
      "Must set ITwinPlatform.defaultAccessToken or ITwinPlatform.defaultShareKey first",
    );
  }
  //>>includeEnd('debug');

  const pageLimit = limit ?? 10000;

  const tilesetUrl = `${ITwinPlatform.apiEndpoint}geospatial-features/itwins/${iTwinId}/ogc/collections/${collectionId}/items`;

  const resource = new Resource({
    url: tilesetUrl,
    headers: {
      Authorization: ITwinPlatform._getAuthorizationHeader(),
      Accept: "application/vnd.bentley.itwin-platform.v1+json",
    },
    queryParameters: {
      limit: pageLimit,
      client: "CesiumJS",
    },
  });

  return GeoJsonDataSource.load(resource);
};

export default ITwinData;
