Source: LayerHDR.js

import { Layer } from './Layer.js'
import { Raster16Bit } from './Raster16Bit.js'
import { ShaderHDR } from './ShaderHDR.js'

/**
 * @typedef {Object} LayerHDROptions
 * @property {string} url - URL of the image to display (required)
 * @property {string|Layout} [layout='image'] - Layout format for image display
 * @property {string} [format='rgba16f'] - Image data format for WebGL processing
 * @property {boolean} [debug=false] - Enable debug output
 * @extends LayerOptions
 */

/**
 * LayerHDR provides advanced HDR image rendering capabilities in OpenLIME.
 * It is designed for high dynamic range (HDR) image processing and rendering,
 * leveraging WebGL shaders and tone mapping techniques.
 * 
 * Features:
 * - HDR tone mapping with configurable white point
 * - WebGL-based rendering with 16-bit precision
 * - Automatic raster data management
 * - Shader-based processing for HDR compression
 * 
 * Technical Details:
 * - Uses WebGL textures for HDR image data
 * - Supports 16-bit float formats (e.g., rgba16f)
 * - Integrates with OpenLIME layout system
 * - Provides multiple tone mapping options: Reinhard, ACES, and Exposure
 * 
 * @extends Layer
 * 
 * @example
 * ```javascript
 * const hdrLayer = new OpenLIME.LayerHDR({
 *   url: 'hdr-image.hdr',
 *   format: 'rgba16f'
 * });
 * viewer.addLayer('hdr', hdrLayer);
 * ```
 */
class LayerHDR extends Layer {
  /**
   * Creates a new LayerHDR instance.
   * 
   * @param {LayerHDROptions} options - Configuration options for the HDR layer
   */
  constructor(options) {
    options = Object.assign({
      format: 'rgba16f',
      autoWhitePoint: true,
      debug: false,
      mode: 'reinhard',
    }, options);
    super(options);

    if (Object.keys(this.rasters).length != 0)
      throw "Rasters options should be empty!";

    if (this.url)
      this.layout.setUrls([this.url]);
    else if (this.layout.urls.length == 0)
      throw "Missing options.url parameter";

    const rasterOptions = {
      format: this.format,
      isLinear: true,  // HDR data is always in linear space
      debug: this.debug
    };
    // Add custom data loader if provided
    if (this.dataLoader) {
      rasterOptions.dataLoader = this.dataLoader;
      rasterOptions.dataLoaderOptions = this.dataLoaderOptions || {};
    }

    let raster = new Raster16Bit(rasterOptions);
    raster.addEvent('loaded', () => {
      if (this.autoWhitePoint) {
        const maxValue = raster.getStatInfo().maxValue ? raster.getStatInfo().maxValue : 1.0;
        this.setWhitePoint(maxValue);
      }
      this.emit('loaded');
    });
    this.rasters.push(raster);

    // Create the HDR shader with all tone mapping parameters
    let shader = new ShaderHDR({
      label: 'HDR',
      format: this.format,
      mode: this.mode || 'reinhard',
    });

    this.shaders = { 'hdr': shader };
    this.setShader('hdr');

    // Reinhard params
    this.addControl('whitePoint', [1.0]);
    this.addControl('shadowLift', [0.0]);
    // ACES params
    this.addControl('acesContrast', [1.2]);
    // Exposure params
    this.addControl('exposure', [1.0]);
    // Balanced params
    this.addControl('highlightCompression', [1.0]);
  }

  /**
   * Sets the white point for HDR tone mapping.
   * 
   * @param {number} v - The new white point value
   * @param {number} [delayms=1] - Delay in milliseconds for the transition
   * @param {string} [easing='linear'] - Easing function for the transition
   */
  setWhitePoint(v, delayms = 1, easing = 'linear') {
    this.setControl('whitePoint', [v], delayms, easing);
  }

  /**
   * Gets the current white point value.
   * 
   * @returns {number} The current white point value
   */
  getWhitePoint() {
    return this.controls['whitePoint'].current.value[0];
  }

  /**
   * Sets the shadow lift value for HDR tone mapping.
   *
   * @param {number} v - The new shadow lift value
   * @param {number} [delayms=1] - Delay in milliseconds for the transition
   * @param {string} [easing='linear'] - Easing function for the transition
   */
  setShadowLift(v, delayms = 1, easing = 'linear') {
    this.setControl('shadowLift', [v], delayms, easing);
  }

  /**
   * Gets the current shadow lift value.
   * 
   * @returns {number} The current shadow lift value
   */
  getShadowLift() {
    return this.controls['shadowLift'].current.value[0];
  }

  /**
   * Sets the ACES contrast parameter for ACES tone mapping.
   * 
   * @param {number} v - The new ACES contrast value
   * @param {number} [delayms=1] - Delay in milliseconds for the transition
   * @param {string} [easing='linear'] - Easing function for the transition
   */
  setAcesContrast(v, delayms = 1, easing = 'linear') {
    this.setControl('acesContrast', [v], delayms, easing);
  }

  /**
   * Gets the current ACES contrast value.
   * 
   * @returns {number} The current ACES contrast value
   */
  getAcesContrast() {
    return this.controls['acesContrast'].current.value[0];
  }

  /**
   * Sets the exposure value for exposure-based tone mapping.
   * 
   * @param {number} v - The new exposure value
   * @param {number} [delayms=1] - Delay in milliseconds for the transition
   * @param {string} [easing='linear'] - Easing function for the transition
   */
  setExposure(v, delayms = 1, easing = 'linear') {
    this.setControl('exposure', [v], delayms, easing);
  }

  /**
   * Gets the current exposure value.
   * 
   * @returns {number} The current exposure value
   */
  getExposure() {
    return this.controls['exposure'].current.value[0];
  }
  /**
   * Sets the highlight compression value for HDR tone mapping.
   * 
   * @param {number} v - The new highlight compression value
   * @param {number} [delayms=1] - Delay in milliseconds for the transition
   * @param {string} [easing='linear'] - Easing function for the transition
   */
  setHighlightCompression(v, delayms = 1, easing = 'linear') {
    this.setControl('highlightCompression', [v], delayms, easing);
  }
  /**
   * Gets the current highlight compression value.
   * 
   * @returns {number} The current highlight compression value
   */
  getHighlightCompression() {
    return this.controls['highlightCompression'].current.value[0];
  }

  /**
   * Retrieves statistical information about the raster data.
   * 
   * @returns {Object} An object containing statistical information (e.g., maxValue, avgLuminance)
   */
  getStatInfo() {
    return this.rasters[0].getStatInfo();
  }

  /**
   * Interpolates control values and updates the shader with the current parameters.
   * 
   * @returns {boolean} Whether the interpolation is complete
   */
  interpolateControls() {
    const done = super.interpolateControls();
    const whitePoint = this.getWhitePoint();
    const shadowLift = this.getShadowLift();
    const acesContrast = this.getAcesContrast();
    const exposure = this.getExposure();
    const highlightCompression = this.getHighlightCompression();

    this.shader.setWhitePoint(whitePoint);
    this.shader.setShadowLift(shadowLift);
    this.shader.setAcesContrast(acesContrast);
    this.shader.setExposure(exposure);
    this.shader.setHighlightCompression(highlightCompression);

    return done;
  }
}

/**
 * Registers this layer type with the Layer factory.
 * 
 * @type {Function}
 * @private
 */
Layer.prototype.types['hdr'] = (options) => { return new LayerHDR(options); }

export { LayerHDR }