Source: LayerMultispectral.js

import { Layer } from './Layer.js'
import { Raster } from './Raster.js'
import { ShaderMultispectral } from './ShaderMultispectral.js'
import { Util } from './Util.js'
import { Transform } from './Transform.js'

/**
 * @typedef {Object} LayerMultispectralOptions
 * @property {string} url - URL to multispectral info.json file (required)
 * @property {string} layout - Layout type: 'image', 'deepzoom', 'google', 'iiif', 'zoomify', 'tarzoom', 'itarzoom'
 * @property {string} [defaultMode='single_band'] - Initial visualization mode ('rgb' or 'single_band')
 * @property {string} [server] - IIP server URL (for IIP layout)
 * @property {boolean} [linearRaster=true] - Whether to use linear color space for rasters (recommended for scientific accuracy)
 * @property {string|Object} presets - Path to presets JSON file or presets object containing CTW configurations
 * @extends LayerOptions
 */

/**
 * LayerMultispectral - Advanced multispectral imagery visualization layer
 * 
 * This layer provides specialized handling of multispectral image data with configurable 
 * visualization modes and interactive spectral analysis capabilities through Color Twist 
 * Weights (CTW). It supports scientific visualization workflows for remote sensing, art analysis,
 * medical imaging, and other multispectral applications.
 * 
 * Features:
 * - Multiple visualization modes (RGB, single band)
 * - UBO-optimized Color Twist Weights implementation for real-time spectral transformations
 * - Preset system for common visualization configurations (false color, etc.)
 * - Support for multiple image layouts and tiling schemes 
 * - Compatible with both single images and tile-based formats (DeepZoom, etc.)
 * 
 * Technical implementation:
 * - Uses WebGL2 features for efficient processing
 * - Implements shader-based visualization pipeline
 * - Supports multiple image layouts and tiling schemes
 * 
 * @extends Layer
 * 
 * @example
 * // Create multispectral layer with deepzoom layout
 * const msLayer = new OpenLIME.Layer({
 *   type: 'multispectral',
 *   url: 'path/to/info.json',
 *   layout: 'deepzoom',
 *   defaultMode: 'rgb',
 *   presets: 'path/to/presets.json'
 * });
 * 
 * // Add to viewer
 * viewer.addLayer('ms', msLayer);
 * 
 * // Apply a preset CTW
 * msLayer.applyPreset('falseColor');
 */
class LayerMultispectral extends Layer {
  /**
   * Creates a new LayerMultispectral instance
   * @param {LayerMultispectralOptions} options - Configuration options
   * @throws {Error} If rasters options is not empty (rasters are created automatically)
   * @throws {Error} If url to info.json is not provided
   * @throws {Error} If presets option is not provided
   */
  constructor(options) {
    super(options);

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

    if (!this.url)
      throw new Error("Url option is required");

    if (!this.presets) {
      throw new Error("Presets option is required");
    }
    this.loadPresets();

    // Set default options
    this.linearRaster = true;
    this.defaultMode = this.defaultMode || 'single_band';

    // Create shader
    this.shaders['multispectral'] = new ShaderMultispectral();
    this.setShader('multispectral');

    // Set current CTW arrays
    this._currentCTW = {
      red: null,
      green: null,
      blue: null
    };

    // Load configuration
    this.info = null;
    this.loadInfo(this.url);
  }

  /**
   * Constructs URL for image resources based on layout type
   * 
   * Handles different image layout conventions including deepzoom, google maps tiles,
   * zoomify, and specialized formats like tarzoom.
   * 
   * @param {string} url - Base URL
   * @param {string} filename - Base filename without extension
   * @returns {string} Complete URL for the resource
   * @private
   */
  imageUrl(url, filename) {
    let path = this.url.substring(0, this.url.lastIndexOf('/') + 1);
    switch (this.layout.type) {
      case 'image': return path + filename + '.jpg';
      case 'google': return path + filename;
      case 'deepzoom':
        // Special handling for multispectral deepzoom
        return path + filename + '.dzi';
      case 'tarzoom': return path + filename + '.tzi';
      case 'itarzoom': return path + filename + '.tzi';
      case 'zoomify': return path + filename + '/ImageProperties.xml';
      case 'iip': return url;
      case 'iiif': throw new Error("Unimplemented");
      default: throw new Error("Unknown layout: " + this.layout.type);
    }
  }

  /**
   * Loads and processes multispectral configuration
   * 
   * Fetches the info.json file containing wavelength, basename, and other
   * configuration parameters, then sets up the rasters and shader accordingly.
   * 
   * @param {string} url - URL to info.json
   * @private
   * @async
   */
  async loadInfo(url) {
    try {
      let infoUrl = url;
      // Need to handle embedded info.json when using IIP and TIFF image stacks
      if (this.layout.type == "iip") infoUrl = (this.server ? this.server + '?FIF=' : '') + url + "&obj=description";

      this.info = await Util.loadJSON(infoUrl);
      console.log("Multispectral info loaded:", this.info);

      // Check if basename is present
      if (!this.info.basename) {
        this.status = "Error: 'basename' is required in the multispectral configuration file";
        console.error(this.status);
        return;
      }

      // Update layout image format and pixelSize if provided in info.json
      if (this.info.format) this.layout.suffix = this.info.format;
      if (this.info.pixelSizeInMM) this.pixelSize = this.info.pixelSizeInMM;

      // Initialize shader with info
      this.shader.init(this.info);

      // Set texture size if available
      if (this.info.width && this.info.height) {
        this.width = this.info.width;
        this.height = this.info.height;
      }

      // Get basename from info
      const baseName = this.info.basename;
      console.log("Using basename:", baseName);

      // Create rasters and URLs array for each image
      const urls = [];

      // Handle special case for itarzoom (all planes in one file)
      if (this.layout.type === 'itarzoom') {
        // Create a single raster for all planes
        let raster = new Raster({ format: 'vec3', isLinear: this.linearRaster });
        this.rasters.push(raster);

        // Add a single URL for all planes
        urls.push(this.imageUrl(url, baseName));
      } else {
        // Standard case: one file per image
        for (let p = 0; p < this.shader.nimg; p++) {
          // Create raster with linear color space
          let raster = new Raster({ format: 'vec3', isLinear: this.linearRaster });
          this.rasters.push(raster);

          // Format index with leading zeros (e.g., 00, 01, 02)
          const indexStr = p.toString().padStart(2, '0');

          // Generate URL for this image
          const imgUrl = this.imageUrl(url, `${baseName}_${indexStr}`);
          urls.push(imgUrl);
          console.log(`Plane ${p} URL: ${imgUrl}`);
        }
      }

      // Set URLs for layout
      if (urls.length > 0) {
        this.layout.setUrls(urls);
      }

      // Set up the shader
      this.setMode(this.defaultMode);
      this.initDefault();

    } catch (e) {
      console.error("Error loading multispectral info:", e);
      this.status = e;
    }
  }

  /**
   * Loads preset definitions for Color Twist Weights
   * 
   * Can load presets from a URL or use directly provided preset object.
   * Presets define predefined CTW configurations for common visualization needs.
   * 
   * @private
   * @async
   */
  async loadPresets() {
    if (typeof this.presets === 'string' && this.presets.trim() !== '') {
      this.presets = await Util.loadJSON(this.presets);
    }
    if (typeof this.presets !== 'object') {
      throw new Error("presets not well formed");
    }
  }

  /**
   * Gets info
   * 
   * @returns {Object|null} Object with info on multispectral dataset or null if not found
   */
  info() {
    return this.info;
  }

  /**
   * Initializes default CTW based on default mode
   * 
   * Creates initial CTW arrays with zeros and applies default
   * visualization settings.
   * 
   * @private
   */
  initDefault() {
    // Create default CTW arrays
    const nplanes = this.shader.nplanes;
    if (!nplanes) return; // Not yet initialized

    let redCTW = new Float32Array(nplanes).fill(0);
    let greenCTW = new Float32Array(nplanes).fill(0);
    let blueCTW = new Float32Array(nplanes).fill(0);

    // Update current CTW and shader
    this._currentCTW.red = redCTW;
    this._currentCTW.green = greenCTW;
    this._currentCTW.blue = blueCTW;

    if (this.defaultMode === 'single-band') {
      this.setSingleBand(0, 0);
    }
  }

  /**
   * Sets the visualization mode
   * 
   * Changes how multispectral data is visualized:
   * - 'rgb': Uses CTW coefficients to create RGB visualization
   * - 'single_band': Shows a single spectral band
   * 
   * @param {string} mode - Mode name ('rgb', 'single_band')
   */
  setMode(mode) {
    if (this.shader) {
      this.shader.setMode(mode);
      this.emit('update');
    }
  }

  /**
   * Sets single band visualization
   * 
   * Displays a single spectral band on a specific output channel.
   * 
   * @param {number} bandIndex - Index of band to visualize
   * @param {number} [channel=0] - Output channel (0=all/gray, 1=R, 2=G, 3=B)
   */
  setSingleBand(bandIndex, channel = 0) {
    if (this.shader) {
      this.shader.setSingleBand(bandIndex, channel);
      this.emit('update');
    }
  }

  /**
   * Sets Color Twist Weights coefficients manually
   * 
   * CTW coefficients define how spectral bands are combined to create
   * RGB visualization. Each array contains weights for each spectral band.
   * 
   * @param {Float32Array} redCTW - Red channel coefficients
   * @param {Float32Array} greenCTW - Green channel coefficients
   * @param {Float32Array} blueCTW - Blue channel coefficients
   * @throws {Error} If arrays have incorrect length
   */
  setCTW(redCTW, greenCTW, blueCTW) {
    if (!this.shader || !this.gl) return;

    // Validate array lengths
    const nplanes = this.shader.nplanes;
    if (redCTW.length !== nplanes || greenCTW.length !== nplanes || blueCTW.length !== nplanes) {
      throw new Error(`CTW arrays must be of length ${nplanes}`);
    }

    // Update current CTW
    this._currentCTW.red = redCTW;
    this._currentCTW.green = greenCTW;
    this._currentCTW.blue = blueCTW;

    // Update shader CTW
    this.shader.setupCTW(this.gl, redCTW, greenCTW, blueCTW);

    // Set to rgb mode
    this.setMode('rgb');
  }

  /**
   * Gets a preset CTW configuration by name
   * 
   * Retrieves the preset's red, green, and blue CTW arrays from
   * the presets collection.
   * 
   * @param {string} presetName - Name of the preset
   * @returns {Object|null} Object with red, green, blue arrays or null if not found
   */
  getPreset(presetName) {
    if (presetName in this.presets) {
      const { red, green, blue } = this.presets[presetName];
      return { red, green, blue };
    } else {
      console.warn(`Preset "${presetName}" not found.`);
      return null;
    }
  }

  /**
   * Applies a preset CTW from the presets library
   * 
   * Loads and applies a predefined set of CTW coefficients for
   * specialized visualization (e.g., false color, vegetation analysis).
   * 
   * @param {string} presetName - Name of the preset
   * @throws {Error} If preset doesn't exist
   */
  applyPreset(presetName) {
    if (!this.shader) return;

    // Get preset from the preset manager
    const preset = this.getPreset(presetName);

    if (!preset) {
      throw new Error(`Preset '${presetName}' not found`);
    }

    // Apply the preset
    this.setCTW(preset.red, preset.green, preset.blue);
  }

  /**
   * Gets the wavelength array for spectral bands
   * 
   * Returns the wavelength values (in nm) for each spectral band.
   * 
   * @returns {number[]} Array of wavelengths
   */
  getWavelengths() {
    return this.shader ? this.shader.wavelength : [];
  }

  /**
   * Gets the number of spectral bands
   * 
   * Returns the count of spectral planes in the multispectral dataset.
   * 
   * @returns {number} Number of bands
   */
  getBandCount() {
    return this.shader ? this.shader.nplanes : 0;
  }

  /**
   * Gets available presets
   * 
   * Returns the names of all available preset CTW configurations.
   * 
   * @returns {string[]} Array of preset names
   */
  getAvailablePresets() {
    return this.presets ? Object.keys(this.presets) : [];
  }

  /**
   * Prepares WebGL resources including UBO for CTW
   * 
   * Sets up WebGL context and ensures CTW arrays are uploaded to GPU.
   * 
   * @override
   * @private
   */
  prepareWebGL() {
    // Call parent implementation
    super.prepareWebGL();

    // Setup CTW if needed
    if (this._currentCTW.red && this.shader && this.gl) {
      this.shader.setupCTW(
        this.gl,
        this._currentCTW.red,
        this._currentCTW.green,
        this._currentCTW.blue
      );
    }
  }

  /**
   * Gets spectrum data for a specific pixel
   * 
   * For tiled formats, this method finds the appropriate tiles
   * and reads the spectral values.
   * 
   * @param {number} x - X coordinate in image space
   * @param {number} y - Y coordinate in image space  
   * @returns {number[]} Array of spectral values (0-100)
   */
  getSpectrum(x, y) {
    // For tiled formats, we need a special approach
    if (this.isTiledFormat()) {
      return this.getTiledSpectrum(x, y);
    }

    // For standard formats, use the original approach
    const pixelData = this.getPixelValues(x, y);
    const spectrum = [];

    if (!pixelData || pixelData.length === 0) {
      return new Array(this.shader.nplanes).fill(0);
    }

    for (let i = 0; i < this.info.nplanes; i++) {
      const idx = Math.floor(i / 3);
      if (idx < pixelData.length) {
        const px = pixelData[idx];
        const pxIdx = i % 3;
        if (px && pxIdx < 3) {
          spectrum.push(px[pxIdx] / 255.0 * 100);
        } else {
          spectrum.push(0);
        }
      } else {
        spectrum.push(0);
      }
    }
    return spectrum;
  }

  /**
   * Checks if current layout is a tiled format
   * @private
   * @returns {boolean} True if using a tiled format
   */
  isTiledFormat() {
    const tiledFormats = ['deepzoom', 'deepzoom1px', 'google', 'zoomify', 'iiif', 'tarzoom'];
    return tiledFormats.includes(this.layout.type);
  }

  /**
   * Gets spectrum data for a specific pixel
   * 
   * Uses the improved getPixelValues method from the base Layer class
   * to obtain spectral values across all bands.
   * 
   * @param {number} x - X coordinate in image space
   * @param {number} y - Y coordinate in image space  
   * @returns {number[]} Array of spectral values (0-100)
   */
  getSpectrum(x, y) {
    // Get pixel data from base Layer class method
    const pixelData = this.getPixelValues(x, y);

    // Create spectrum array for all planes
    const spectrum = new Array(this.shader.nplanes).fill(0);

    // If no data was returned, return empty spectrum
    if (!pixelData || pixelData.length === 0) {
      return spectrum;
    }

    // Convert pixel data to spectrum values
    for (let i = 0; i < this.shader.nplanes; i++) {
      const idx = Math.floor(i / 3); // Which texture contains this band
      const pxIdx = i % 3;           // Which channel (R, G, B) in the texture

      if (idx < pixelData.length) {
        const px = pixelData[idx];
        if (px && pxIdx < 3) {
          // Convert to percentage (0-100)
          spectrum[i] = px[pxIdx] / 255.0 * 100;
        }
      }
    }

    return spectrum;
  }

}

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

export { LayerMultispectral }