import BoundingSphere from "./BoundingSphere.js";
import buildModuleUrl from "./buildModuleUrl.js";
import Cartesian2 from "./Cartesian2.js";
import Cartesian3 from "./Cartesian3.js";
import Cartographic from "./Cartographic.js";
import Check from "./Check.js";
import defaultValue from "./defaultValue.js";
import defined from "./defined.js";
import DeveloperError from "./DeveloperError.js";
import Ellipsoid from "./Ellipsoid.js";
import GeographicTilingScheme from "./GeographicTilingScheme.js";
import Rectangle from "./Rectangle.js";
import Resource from "./Resource.js";

const scratchDiagonalCartesianNE = new Cartesian3();
const scratchDiagonalCartesianSW = new Cartesian3();
const scratchDiagonalCartographic = new Cartographic();
const scratchCenterCartesian = new Cartesian3();
const scratchSurfaceCartesian = new Cartesian3();

const scratchBoundingSphere = new BoundingSphere();
const tilingScheme = new GeographicTilingScheme();
const scratchCorners = [
  new Cartographic(),
  new Cartographic(),
  new Cartographic(),
  new Cartographic(),
];
const scratchTileXY = new Cartesian2();

/**
 * A collection of functions for approximating terrain height
 * @private
 */
const ApproximateTerrainHeights = {};

/**
 * Initializes the minimum and maximum terrain heights
 * @return {Promise.<void>}
 */
ApproximateTerrainHeights.initialize = function () {
  let initPromise = ApproximateTerrainHeights._initPromise;
  if (defined(initPromise)) {
    return initPromise;
  }

  initPromise = Resource.fetchJson(
    buildModuleUrl("Assets/approximateTerrainHeights.json")
  ).then(function (json) {
    ApproximateTerrainHeights._terrainHeights = json;
  });
  ApproximateTerrainHeights._initPromise = initPromise;

  return initPromise;
};

/**
 * Computes the minimum and maximum terrain heights for a given rectangle
 * @param {Rectangle} rectangle The bounding rectangle
 * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid
 * @return {{minimumTerrainHeight: Number, maximumTerrainHeight: Number}}
 */
ApproximateTerrainHeights.getMinimumMaximumHeights = function (
  rectangle,
  ellipsoid
) {
  //>>includeStart('debug', pragmas.debug);
  Check.defined("rectangle", rectangle);
  if (!defined(ApproximateTerrainHeights._terrainHeights)) {
    throw new DeveloperError(
      "You must call ApproximateTerrainHeights.initialize and wait for the promise to resolve before using this function"
    );
  }
  //>>includeEnd('debug');
  ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84);

  const xyLevel = getTileXYLevel(rectangle);

  // Get the terrain min/max for that tile
  let minTerrainHeight = ApproximateTerrainHeights._defaultMinTerrainHeight;
  let maxTerrainHeight = ApproximateTerrainHeights._defaultMaxTerrainHeight;
  if (defined(xyLevel)) {
    const key = `${xyLevel.level}-${xyLevel.x}-${xyLevel.y}`;
    const heights = ApproximateTerrainHeights._terrainHeights[key];
    if (defined(heights)) {
      minTerrainHeight = heights[0];
      maxTerrainHeight = heights[1];
    }

    // Compute min by taking the center of the NE->SW diagonal and finding distance to the surface
    ellipsoid.cartographicToCartesian(
      Rectangle.northeast(rectangle, scratchDiagonalCartographic),
      scratchDiagonalCartesianNE
    );
    ellipsoid.cartographicToCartesian(
      Rectangle.southwest(rectangle, scratchDiagonalCartographic),
      scratchDiagonalCartesianSW
    );

    Cartesian3.midpoint(
      scratchDiagonalCartesianSW,
      scratchDiagonalCartesianNE,
      scratchCenterCartesian
    );
    const surfacePosition = ellipsoid.scaleToGeodeticSurface(
      scratchCenterCartesian,
      scratchSurfaceCartesian
    );
    if (defined(surfacePosition)) {
      const distance = Cartesian3.distance(
        scratchCenterCartesian,
        surfacePosition
      );
      minTerrainHeight = Math.min(minTerrainHeight, -distance);
    } else {
      minTerrainHeight = ApproximateTerrainHeights._defaultMinTerrainHeight;
    }
  }

  minTerrainHeight = Math.max(
    ApproximateTerrainHeights._defaultMinTerrainHeight,
    minTerrainHeight
  );

  return {
    minimumTerrainHeight: minTerrainHeight,
    maximumTerrainHeight: maxTerrainHeight,
  };
};

/**
 * Computes the bounding sphere based on the tile heights in the rectangle
 * @param {Rectangle} rectangle The bounding rectangle
 * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid
 * @return {BoundingSphere} The result bounding sphere
 */
ApproximateTerrainHeights.getBoundingSphere = function (rectangle, ellipsoid) {
  //>>includeStart('debug', pragmas.debug);
  Check.defined("rectangle", rectangle);
  if (!defined(ApproximateTerrainHeights._terrainHeights)) {
    throw new DeveloperError(
      "You must call ApproximateTerrainHeights.initialize and wait for the promise to resolve before using this function"
    );
  }
  //>>includeEnd('debug');
  ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84);

  const xyLevel = getTileXYLevel(rectangle);

  // Get the terrain max for that tile
  let maxTerrainHeight = ApproximateTerrainHeights._defaultMaxTerrainHeight;
  if (defined(xyLevel)) {
    const key = `${xyLevel.level}-${xyLevel.x}-${xyLevel.y}`;
    const heights = ApproximateTerrainHeights._terrainHeights[key];
    if (defined(heights)) {
      maxTerrainHeight = heights[1];
    }
  }

  const result = BoundingSphere.fromRectangle3D(rectangle, ellipsoid, 0.0);
  BoundingSphere.fromRectangle3D(
    rectangle,
    ellipsoid,
    maxTerrainHeight,
    scratchBoundingSphere
  );

  return BoundingSphere.union(result, scratchBoundingSphere, result);
};

function getTileXYLevel(rectangle) {
  Cartographic.fromRadians(
    rectangle.east,
    rectangle.north,
    0.0,
    scratchCorners[0]
  );
  Cartographic.fromRadians(
    rectangle.west,
    rectangle.north,
    0.0,
    scratchCorners[1]
  );
  Cartographic.fromRadians(
    rectangle.east,
    rectangle.south,
    0.0,
    scratchCorners[2]
  );
  Cartographic.fromRadians(
    rectangle.west,
    rectangle.south,
    0.0,
    scratchCorners[3]
  );

  // Determine which tile the bounding rectangle is in
  let lastLevelX = 0,
    lastLevelY = 0;
  let currentX = 0,
    currentY = 0;
  const maxLevel = ApproximateTerrainHeights._terrainHeightsMaxLevel;
  let i;
  for (i = 0; i <= maxLevel; ++i) {
    let failed = false;
    for (let j = 0; j < 4; ++j) {
      const corner = scratchCorners[j];
      tilingScheme.positionToTileXY(corner, i, scratchTileXY);
      if (j === 0) {
        currentX = scratchTileXY.x;
        currentY = scratchTileXY.y;
      } else if (currentX !== scratchTileXY.x || currentY !== scratchTileXY.y) {
        failed = true;
        break;
      }
    }

    if (failed) {
      break;
    }

    lastLevelX = currentX;
    lastLevelY = currentY;
  }

  if (i === 0) {
    return undefined;
  }

  return {
    x: lastLevelX,
    y: lastLevelY,
    level: i > maxLevel ? maxLevel : i - 1,
  };
}

ApproximateTerrainHeights._terrainHeightsMaxLevel = 6;
ApproximateTerrainHeights._defaultMaxTerrainHeight = 9000.0;
ApproximateTerrainHeights._defaultMinTerrainHeight = -100000.0;
ApproximateTerrainHeights._terrainHeights = undefined;
ApproximateTerrainHeights._initPromise = undefined;

Object.defineProperties(ApproximateTerrainHeights, {
  /**
   * Determines if the terrain heights are initialized and ready to use. To initialize the terrain heights,
   * call {@link ApproximateTerrainHeights#initialize} and wait for the returned promise to resolve.
   * @type {Boolean}
   * @readonly
   * @memberof ApproximateTerrainHeights
   */
  initialized: {
    get: function () {
      return defined(ApproximateTerrainHeights._terrainHeights);
    },
  },
});
export default ApproximateTerrainHeights;
