# Source: color.js

    import colormap from 'colormap'

    import { _generateUID, rescale } from './utils.js'

    const _attrs = Symbol('attrs')

    /**
     * Enumerated values for color map names.
     *
     * @memberof color
     */
    const ColormapNames = {
      VIRIDIS: 'VIRIDIS',
      INFERNO: 'INFERNO',
      MAGMA: 'MAGMA',
      GRAY: 'GRAY',
      BLUE_RED: 'BLUE_RED',
      PHASE: 'PHASE',
      PORTLAND: 'PORTLAND',
      HOT: 'HOT'
    }
    Object.freeze(ColormapNames)

    /**
     * Create a color map.
     *
     * @param {Object} options
     * @param {string} options.name - Name of the color map
     * @param {string} options.bins - Number of color bins
     *
     * @returns {number[][]} RGB triplet for each color
     *
     * @memberof color
     */
    function createColormap ({ name, bins }) {
      const lut = {
        INFERNO: ['inferno', false],
        MAGMA: ['magma', false],
        VIRIDIS: ['viridis', false],
        GRAY: ['greys', false],
        BLUE_RED: ['RdBu', false],
        PHASE: ['phase', true],
        PORTLAND: ['portland', false],
        HOT: ['HOT', false]
      }
      const params = lut[name]
      if (params === undefined) {
        throw new Error(`Unknown colormap "${name}".`)
      }

      const internalName = params[0]
      const reverse = params[1]
      const colors = colormap({
        colormap: internalName,
        nshades: bins,
        format: 'rgb'
      })
      if (reverse) {
        return colors.reverse()
      }
      return colors
    }

    /**
     * Build a palette color lookup table object from a colormap.
     *
     * @param {Object} options
     * @param {number[][]} options.data - Array of RGB triplets for each color
     * @param {number} options.firstValueMapped - First value that should be mapped
     *
     * @returns {color.PaletteColorLookupTable} Mapping of grayscale pixel values to RGB color triplets
     *
     * @memberof color
     */
    function buildPaletteColorLookupTable ({ data, firstValueMapped }) {
      if (data == null) {
        throw new Error(
          'Argument "data" is required for building Palette Color Lookup Table.'
        )
      }
      if (firstValueMapped == null) {
        throw new Error(
          'Argument "firstValueMapped" is required for building ' +
          'Palette Color Lookup Table.'
        )
      }

      const numberOfEntries = data.length

      const Type = Uint8Array
      const redData = new Type(numberOfEntries)
      const greenData = new Type(numberOfEntries)
      const blueData = new Type(numberOfEntries)
      for (let i = 0; i < numberOfEntries; i++) {
        redData[i] = data[i][0]
        greenData[i] = data[i][1]
        blueData[i] = data[i][2]
      }

      const descriptor = [numberOfEntries, firstValueMapped, 8]

      return new PaletteColorLookupTable({
        uid: _generateUID(),
        redDescriptor: descriptor,
        greenDescriptor: descriptor,
        blueDescriptor: descriptor,
        redData,
        greenData,
        blueData
      })
    }

    /**
     * A Palette Color Lookup Table
     *
     * @class
     * @memberof color
     */
    class PaletteColorLookupTable {
      /**
       * @param {Object} options
       * @param {string} options.uid - UID
       * @param {number[]} options.redDescriptor - Red LUT descriptor
       * @param {number[]} options.greenDescriptor - Green LUT descriptor
       * @param {number[]} options.blueDescriptor - Blue LUT descriptor
       * @param {Uint8Array|Uint16Array} options.redData - Red LUT data
       * @param {Uint8Array|Uint16Array} options.greenData - Green LUT data
       * @param {Uint8Array|Uint16Array} options.blueData - Blue LUT data
       * @param {Uint8Array|Uint16Array} options.redSegmentedData - Red segmented LUT data
       * @param {Uint8Array|Uint16Array} options.greenSegmentedData - Green segmented LUT data
       * @param {Uint8Array|Uint16Array} options.blueSegmentedData - Blue segmented LUT data
       */
      constructor ({
        uid,
        redDescriptor,
        greenDescriptor,
        blueDescriptor,
        redData,
        greenData,
        blueData,
        redSegmentedData,
        greenSegmentedData,
        blueSegmentedData
      }) {
        this[_attrs] = { uid }

        // Number of entries in the LUT data
        const firstDescriptorValues = new Set([
          redDescriptor[0],
          greenDescriptor[0],
          blueDescriptor[0]
        ])
        if (firstDescriptorValues.size !== 1) {
          throw new Error(
            'First value of Red, Green, and Blue Palette Color Lookup Table ' +
            'Descriptor must be the same.'
          )
        }
        const n = [...firstDescriptorValues][0]
        if (n === 0) {
          this[_attrs].numberOfEntries = Math.pow(2, 16)
        } else {
          this[_attrs].numberOfEntries = n
        }

        // Pixel value mapped to the first entry in the LUT data
        const secondDescriptorValues = new Set([
          redDescriptor[1],
          greenDescriptor[1],
          blueDescriptor[1]
        ])
        if (secondDescriptorValues.size !== 1) {
          throw new Error(
            'Second value of Red, Green, and Blue Palette Color Lookup Table ' +
            'Descriptor must be the same.'
          )
        }
        this[_attrs].firstValueMapped = [...secondDescriptorValues][0]

        // Number of bits for each entry in the LUT Data
        const thirdDescriptorValues = new Set([
          redDescriptor[2],
          greenDescriptor[2],
          blueDescriptor[2]
        ])
        if (thirdDescriptorValues.size !== 1) {
          throw new Error(
            'Third value of Red, Green, and Blue Palette Color Lookup Table ' +
            'Descriptor must be the same.'
          )
        }
        this[_attrs].bitsPerEntry = [...thirdDescriptorValues][0]
        if ([8, 16].indexOf(this[_attrs].bitsPerEntry) < 0) {
          throw new Error(
            'Third value of Red, Green, and Blue Palette Color Lookup Table ' +
            'Descriptor must be either ' + '8 or 16.'
          )
        }

        if (redSegmentedData != null && redData != null) {
          throw new Error(
            'Either Segmented Red Palette Color Lookup Data or Red Palette ' +
            'Color Lookup Data should be provided, but not both.'
          )
        } else if (redSegmentedData == null && redData == null) {
          throw new Error(
            'Either Segmented Red Palette Color Lookup Data or Red Palette ' +
            'Color Lookup Data must be provided.'
          )
        }
        if (redData) {
          if (redData.length !== this[_attrs].numberOfEntries) {
            throw new Error(
              'Red Palette Color Lookup Table Data has wrong number of entries.'
            )
          }
        }
        this[_attrs].redSegmentedData = redSegmentedData
        this[_attrs].redData = redData

        if (greenSegmentedData != null && greenData != null) {
          throw new Error(
            'Either Segmented Green Palette Color Lookup Data or Green Palette ' +
            'Color Lookup Data should be provided, but not both.'
          )
        } else if (greenSegmentedData == null && greenData == null) {
          throw new Error(
            'Either Segmented Green Palette Color Lookup Data or Green ' +
            'Palette Color Lookup Data must be provided.'
          )
        }
        if (greenData) {
          if (greenData.length !== this[_attrs].numberOfEntries) {
            throw new Error(
              'Green Palette Color Lookup Table Data has wrong number of entries.'
            )
          }
        }
        this[_attrs].greenSegmentedData = greenSegmentedData
        this[_attrs].greenData = greenData

        if (blueSegmentedData != null && blueData != null) {
          throw new Error(
            'Either Segmented Blue Palette Color Lookup Data or Blue Palette ' +
            'Color Lookup Data must be provided, but not both.'
          )
        } else if (blueSegmentedData != null && blueData != null) {
          throw new Error(
            'Either Segmented Blue Palette Color Lookup Data or Blue Palette ' +
            'Color Lookup Data must be provided.'
          )
        }
        if (blueData) {
          if (blueData.length !== this[_attrs].numberOfEntries) {
            throw new Error(
              'Blue Palette Color Lookup Table Data has wrong number of entries.'
            )
          }
        }
        this[_attrs].blueSegmentedData = blueSegmentedData
        this[_attrs].blueData = blueData

        if (this[_attrs].bitsPerEntry === 8) {
          this[_attrs].DataType = Uint8Array
        } else {
          this[_attrs].DataType = Uint16Array
        }

        // Will be used to cache created colormap for repeated access
        this[_attrs].data = null

        Object.freeze(this)
      }

      _expandSegmentedLUTData (segmentedData) {
        const lut = new this[_attrs].DataType(this[_attrs].numberOfEntries)
        let offset = 0
        for (let i = 0; i < segmentedData.length; i++) {
          const opcode = segmentedData[i++]
          if (opcode === 0) {
            // Discrete
            const length = segmentedData[i++]
            const value = segmentedData[i]
            for (let j = offset; j < (offset + length); j++) {
              lut[j] = value
            }
            offset += length
          } else if (opcode === 1) {
            // Linear (interpolation)
            const length = segmentedData[i++]
            const endpoint = segmentedData[i]
            const startpoint = lut[offset - 1]
            const step = (endpoint - startpoint) / (length - 1)
            for (let j = 0; j < length; j++) {
              const value = startpoint + Math.round(j * step)
              lut[offset + j] = value
            }
            offset += length
          } else if (opcode === 2) {
            // TODO
            throw new Error(
              'Indirect segment type is not yet supported for ' +
              'Segmented Palette Color Lookup Table.'
            )
          } else {
            throw new Error(
              'Encountered unexpected segment type is not yet supported for ' +
              'Segmented Palette Color Lookup Table.'
            )
          }
        }
        return lut
      }

      /**
       * Palette Color Lookup Table UID
       *
       * @type string
       */
      get uid () {
        return this[_attrs].uid
      }

      /**
       * Palette Color Lookup Table Data.
       *
       * RGB color triplet for each value mapped.
       *
       * @type number[][]
       */
      get data () {
        if (this[_attrs].data == null) {
          const redLUT = (
            this[_attrs].redData
              ? new this[_attrs].DataType(this[_attrs].redData)
              : this._expandSegmentedLUTData(
                this[_attrs].redSegmentedData,
                this[_attrs].numberOfEntries,
                this[_attrs].bitsPerEntry
              )
          )
          const greenLUT = (
            this[_attrs].greenData
              ? new this[_attrs].DataType(this[_attrs].greenData)
              : this._expandSegmentedLUTData(
                this[_attrs].greenSegmentedData,
                this[_attrs].numberOfEntries,
                this[_attrs].bitsPerEntry
              )
          )
          const blueLUT = (
            this[_attrs].blueData
              ? new this[_attrs].DataType(this[_attrs].blueData)
              : this._expandSegmentedLUTData(
                this[_attrs].blueSegmentedData,
                this[_attrs].numberOfEntries,
                this[_attrs].bitsPerEntry
              )
          )
          const uniqueNumberOfEntries = new Set([
            redLUT.length,
            greenLUT.length,
            blueLUT.length
          ])
          if (uniqueNumberOfEntries.size > 1) {
            throw new Error(
              'Red, Green, and Blue Palette Color Lookup Tables ' +
              'must have the same size.'
            )
          }

          const maxValues = [
            Math.max(...redLUT),
            Math.max(...greenLUT),
            Math.max(...blueLUT)
          ]
          const maxInput = Math.max(...maxValues)
          const maxOutput = 255
          if (this[_attrs].bitsPerEntry === 16 && maxInput > 255) {
            /*
             * Only palettes with 256 entries and 8 bit per entry are supported for
             * display.  Therefore, data need to rescaled and resampled.
             */
            const n = 256
            const step = this[_attrs].numberOfEntries / n
            this[_attrs].data = new Array(n)
            for (let i = 0; i < n; i++) {
              const j = i * step
              this[_attrs].data[i] = [
                Math.round(rescale(redLUT[j], 0, maxInput, 0, maxOutput)),
                Math.round(rescale(greenLUT[j], 0, maxInput, 0, maxOutput)),
                Math.round(rescale(blueLUT[j], 0, maxInput, 0, maxOutput))
              ]
            }
          } else {
            this[_attrs].data = new Array(this[_attrs].numberOfEntries)
            for (let i = 0; i < this[_attrs].numberOfEntries; i++) {
              this[_attrs].data[i] = [redLUT[i], greenLUT[i], blueLUT[i]]
            }
          }
        }
        return this[_attrs].data
      }

      /**
       * First value mapped
       *
       * @type number
       */
      get firstValueMapped () {
        return this[_attrs].firstValueMapped
      }
    }

    export {
      ColormapNames,
      createColormap,
      PaletteColorLookupTable,
      buildPaletteColorLookupTable
    }