import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
import JulianDate from "../Core/JulianDate.js";
import CesiumMath from "../Core/Math.js";

/**
 * A heatmap colorizer in a {@link Cesium3DTileset}. A tileset can colorize its visible tiles in a heatmap style.
 *
 * @alias Cesium3DTilesetHeatmap
 * @constructor
 * @private
 */
function Cesium3DTilesetHeatmap(tilePropertyName) {
  /**
   * The tile variable to track for heatmap colorization.
   * Tile's will be colorized relative to the other visible tile's values for this variable.
   *
   * @type {string}
   */
  this.tilePropertyName = tilePropertyName;

  // Members that are updated every time a tile is colorized
  this._minimum = Number.MAX_VALUE;
  this._maximum = -Number.MAX_VALUE;

  // Members that are updated once every frame
  this._previousMinimum = Number.MAX_VALUE;
  this._previousMaximum = -Number.MAX_VALUE;

  // If defined uses a reference minimum maximum to colorize by instead of using last frames minimum maximum of rendered tiles.
  // For example, the _loadTimestamp can get a better colorization using setReferenceMinimumMaximum in order to take accurate colored timing diffs of various scenes.
  this._referenceMinimum = {};
  this._referenceMaximum = {};
}

/**
 * Convert to a usable heatmap value (i.e. a number). Ensures that tile values that aren't stored as numbers can be used for colorization.
 * @private
 */
function getHeatmapValue(tileValue, tilePropertyName) {
  let value;
  if (tilePropertyName === "_loadTimestamp") {
    value = JulianDate.toDate(tileValue).getTime();
  } else {
    value = tileValue;
  }
  return value;
}

/**
 * Sets the reference minimum and maximum for the variable name. Converted to numbers before they are stored.
 *
 * @param {object} minimum The minimum reference value.
 * @param {object} maximum The maximum reference value.
 * @param {string} tilePropertyName The tile variable that will use these reference values when it is colorized.
 */
Cesium3DTilesetHeatmap.prototype.setReferenceMinimumMaximum = function (
  minimum,
  maximum,
  tilePropertyName,
) {
  this._referenceMinimum[tilePropertyName] = getHeatmapValue(
    minimum,
    tilePropertyName,
  );
  this._referenceMaximum[tilePropertyName] = getHeatmapValue(
    maximum,
    tilePropertyName,
  );
};

function getHeatmapValueAndUpdateMinimumMaximum(heatmap, tile) {
  const tilePropertyName = heatmap.tilePropertyName;
  if (defined(tilePropertyName)) {
    const heatmapValue = getHeatmapValue(
      tile[tilePropertyName],
      tilePropertyName,
    );
    if (!defined(heatmapValue)) {
      heatmap.tilePropertyName = undefined;
      return heatmapValue;
    }
    heatmap._maximum = Math.max(heatmapValue, heatmap._maximum);
    heatmap._minimum = Math.min(heatmapValue, heatmap._minimum);
    return heatmapValue;
  }
}

const heatmapColors = [
  new Color(0.1, 0.1, 0.1, 1), // Dark Gray
  new Color(0.153, 0.278, 0.878, 1), // Blue
  new Color(0.827, 0.231, 0.49, 1), // Pink
  new Color(0.827, 0.188, 0.22, 1), // Red
  new Color(1.0, 0.592, 0.259, 1), // Orange
  new Color(1.0, 0.843, 0.0, 1),
]; // Yellow
/**
 * Colorize the tile in heat map style based on where it lies within the minimum maximum window.
 * Heatmap colors are black, blue, pink, red, orange, yellow. 'Cold' or low numbers will be black and blue, 'Hot' or high numbers will be orange and yellow,
 * @param {Cesium3DTile} tile The tile to colorize relative to last frame's minimum and maximum values of all visible tiles.
 * @param {FrameState} frameState The frame state.
 */
Cesium3DTilesetHeatmap.prototype.colorize = function (tile, frameState) {
  const tilePropertyName = this.tilePropertyName;
  if (
    !defined(tilePropertyName) ||
    !tile.contentAvailable ||
    tile._selectedFrame !== frameState.frameNumber
  ) {
    return;
  }

  const heatmapValue = getHeatmapValueAndUpdateMinimumMaximum(this, tile);
  const minimum = this._previousMinimum;
  const maximum = this._previousMaximum;

  if (minimum === Number.MAX_VALUE || maximum === -Number.MAX_VALUE) {
    return;
  }

  // Shift the minimum maximum window down to 0
  const shiftedMax = maximum - minimum + CesiumMath.EPSILON7; // Prevent divide by 0
  const shiftedValue = CesiumMath.clamp(
    heatmapValue - minimum,
    0.0,
    shiftedMax,
  );

  // Get position between minimum and maximum and convert that to a position in the color array
  const zeroToOne = shiftedValue / shiftedMax;
  const lastIndex = heatmapColors.length - 1.0;
  const colorPosition = zeroToOne * lastIndex;

  // Take floor and ceil of the value to get the two colors to lerp between, lerp using the fractional portion
  const colorPositionFloor = Math.floor(colorPosition);
  const colorPositionCeil = Math.ceil(colorPosition);
  const t = colorPosition - colorPositionFloor;
  const colorZero = heatmapColors[colorPositionFloor];
  const colorOne = heatmapColors[colorPositionCeil];

  // Perform the lerp
  const finalColor = Color.clone(Color.WHITE);
  finalColor.red = CesiumMath.lerp(colorZero.red, colorOne.red, t);
  finalColor.green = CesiumMath.lerp(colorZero.green, colorOne.green, t);
  finalColor.blue = CesiumMath.lerp(colorZero.blue, colorOne.blue, t);
  tile._debugColor = finalColor;
};

/**
 * Resets the tracked minimum maximum values for heatmap colorization. Happens right before tileset traversal.
 */
Cesium3DTilesetHeatmap.prototype.resetMinimumMaximum = function () {
  // For heat map colorization
  const tilePropertyName = this.tilePropertyName;
  if (defined(tilePropertyName)) {
    const referenceMinimum = this._referenceMinimum[tilePropertyName];
    const referenceMaximum = this._referenceMaximum[tilePropertyName];
    const useReference = defined(referenceMinimum) && defined(referenceMaximum);
    this._previousMinimum = useReference ? referenceMinimum : this._minimum;
    this._previousMaximum = useReference ? referenceMaximum : this._maximum;
    this._minimum = Number.MAX_VALUE;
    this._maximum = -Number.MAX_VALUE;
  }
};
export default Cesium3DTilesetHeatmap;
