import * as cmaps from '@/cmaps'
import { log } from '@/logger'

export enum COLORMAP_TYPE {
    MIN_TO_MAX = 0,
    ZERO_TO_MAX_TRANSPARENT_BELOW_MIN = 1,
    ZERO_TO_MAX_TRANSLUCENT_BELOW_MIN = 2
}

export type ColorMap = {
    R: number[]
    G: number[]
    B: number[]
    A: number[]
    I: number[]

    min?: number
    max?: number
    labels?: string[]
}

export type LUT = {
    lut: Uint8ClampedArray
    min?: number
    max?: number
    labels?: string[]
}

export class ColorTables {
    gamma = 1.0
    version = 0.1
    cluts: Record<string, ColorMap> = {}

    /**
     * Sets cluts to alphabetically sorted cmaps
     */
    constructor() {
        const cmapKeys = Object.keys(cmaps) as Array<keyof typeof cmaps>

        const cmapsSorted = cmapKeys
            .filter((k) => !k.startsWith('$')) // ignore drawing maps
            .sort(new Intl.Collator('en').compare) // case insensitive, e.g. "ROI_i256" > "actc"

        for (const key of cmapsSorted) {
            this.cluts[key] = cmaps[key]
        }
    }

    addColormap(key: string, cmap: ColorMap): void {
        this.cluts[key] = cmap
    }

    colormaps(): Array<keyof typeof this.cluts> {
        return Object.keys(this.cluts) as Array<keyof typeof this.cluts>
    }

    // for backward compatibility: prior to v0.34 "colormaps" used to be "colorMaps"
    colorMaps(): Array<keyof typeof this.cluts> {
        return this.colormaps()
    }

    // returns key name if it exists, otherwise returns default "gray"
    colormapFromKey(name: string): ColorMap {
        let cmap = this.cluts[name]
        if (cmap !== undefined) {
            return cmap
        }

        cmap = this.cluts[name.toLowerCase()]
        if (cmap !== undefined) {
            return cmap
        }

        if (name.length > 0) {
            log.warn('No color map named ' + name)
        }
        return {
            min: 0,
            max: 0,
            R: [0, 255],
            G: [0, 255],
            B: [0, 255],
            A: [0, 255],
            I: [0, 255]
        }
    }

    // not included in public docs
    colormap(key = '', isInvert = false): Uint8ClampedArray {
        const cmap = this.colormapFromKey(key)
        return this.makeLut(cmap.R, cmap.G, cmap.B, cmap.A, cmap.I, isInvert)
    }

    makeLabelLut(cm: ColorMap, alphaFill = 255, maxIdx = Infinity): LUT {
        if (cm.R === undefined || cm.G === undefined || cm.B === undefined) {
            throw new Error(`Invalid colormap table: ${cm}`)
        }
        const nLabels = cm.R.length

        // if indices are not provided, indices default to 0..(nLabels-1)
        const idxs = cm.I ?? [...Array(nLabels).keys()]

        // --- Check for out-of-range values: issue 1441
        let hasInvalid = false
        for (let i = 0; i < idxs.length; i++) {
            if (idxs[i] > maxIdx) {
                hasInvalid = true
                idxs[i] = maxIdx // clamp
            }
        }
        if (hasInvalid) {
            log.warn(`Some colormap indices clamped to match label range.`)
        }

        if (nLabels !== cm.G.length || nLabels !== cm.B.length || nLabels !== idxs.length) {
            throw new Error(`colormap does not make sense: ${cm} Rs ${cm.R.length} Gs ${cm.G.length} Bs ${cm.B.length} Is ${idxs.length}`)
        }

        let As = new Uint8ClampedArray(nLabels).fill(alphaFill)
        const zeroPos = idxs.indexOf(0)
        if (zeroPos >= 0) {
            As[zeroPos] = 0
        }
        if (cm.A !== undefined) {
            As = Uint8ClampedArray.from(cm.A)
        }

        const mnIdx = Math.min(...idxs)
        const mxIdx = Math.max(...idxs)

        // n.b. number of input labels can be sparse: I:[0,3,4] output is dense [0,1,2,3,4]
        const nLabelsDense = mxIdx - mnIdx + 1
        const lut = new Uint8ClampedArray(nLabelsDense * 4).fill(0)
        for (let i = 0; i < nLabels; i++) {
            let k = (idxs[i] - mnIdx) * 4
            lut[k++] = cm.R[i] // Red
            lut[k++] = cm.G[i] // Green
            lut[k++] = cm.B[i] // Blue
            lut[k++] = As[i] // Alpha
        }

        const cmap: LUT = {
            lut,
            min: mnIdx,
            max: mxIdx
        }
        // labels are optional
        if (cm.labels) {
            const nL = cm.labels.length
            if (nL === nLabelsDense) {
                cmap.labels = cm.labels
            } else if (nL === nLabels) {
                cmap.labels = Array(nLabelsDense).fill('?')
                for (let i = 0; i < nLabels; i++) {
                    cmap.labels[idxs[i] - mnIdx] = cm.labels[i]
                }
            }
        }
        return cmap
    }

    async makeLabelLutFromUrl(name: string, alphaFill = 255, maxIdx = Infinity): Promise<LUT> {
        const response = await fetch(name)
        const cm = await response.json()
        return this.makeLabelLut(cm, alphaFill, maxIdx)
    }

    // not included in public docs
    // The drawing colormap is a variant of the label colormap with precisely 256 colors
    makeDrawLut(name: string | ColorMap): LUT {
        let cmap: ColorMap = typeof name === 'object' ? name : cmaps[name as keyof typeof cmaps]

        if (cmap === undefined) {
            log.warn('colormap undefined ', name)
            cmap = this.colormapFromKey('')
        }

        const cm = this.makeLabelLut(cmap, 255)

        if (cm.labels === undefined) {
            cm.labels = []
        }
        if (cm.labels.length < 256) {
            const j = cm.labels.length
            for (let i = j; i < 256; i++) {
                // make all unused slots opaque red
                cm.labels.push(i.toString())
            }
        }

        const lut = new Uint8ClampedArray(256 * 4)
        let k = 0
        for (let i = 0; i < 256; i++) {
            lut[k++] = 255 // Red
            lut[k++] = 0 // Green
            lut[k++] = 0 // Blue
            lut[k++] = 255 // Alpha
        }
        lut[3] = 0 // make first alpha transparent: not part of drawing

        // drawings can have no more than 256 colors
        const explicitLUTbytes = Math.min(cm.lut.length, 256 * 4)
        if (explicitLUTbytes > 0) {
            for (let i = 0; i < explicitLUTbytes; i++) {
                lut[i] = cm.lut[i]
            }
        }

        return {
            lut,
            labels: cm.labels
        }
    }

    // not included in public docs
    makeLut(Rsi: number[], Gsi: number[], Bsi: number[], Asi: number[], Isi: number[], isInvert: boolean): Uint8ClampedArray {
        // create color lookup table provided arrays of reds, greens, blues, alphas and intensity indices
        // intensity indices should be in increasing order with the first value 0 and the last 255.
        // this.makeLut([0, 255], [0, 0], [0,0], [0,128],[0,255]); //red gradient
        const nIdx = Rsi.length
        const Rs = [...Rsi]
        const Gs = [...Gsi]
        const Bs = [...Bsi]
        if (!Isi) {
            Isi = new Array(nIdx)
            for (let i = 0; i < nIdx; i++) {
                Isi[i] = (i / (nIdx - 1)) * 255
            }
        }
        if (!Asi) {
            Asi = new Array(nIdx).fill(64)
            Asi[0] = 0
        }
        let As = Uint8ClampedArray.from(Asi)
        let Is = Uint8ClampedArray.from(Isi)
        if (isInvert) {
            for (let i = 0; i < nIdx; i++) {
                Rs[i] = Rsi[nIdx - 1 - i]
                Gs[i] = Gsi[nIdx - 1 - i]
                Bs[i] = Bsi[nIdx - 1 - i]
                As[i] = 255 - Asi[nIdx - 1 - i]
                Is[i] = 255 - Isi[nIdx - 1 - i]
            }
        }
        const lut = new Uint8ClampedArray(256 * 4)
        if (typeof Is === 'undefined') {
            Is = new Uint8ClampedArray(nIdx).fill(0)
            for (let i = 0; i < nIdx; i++) {
                Is[i] = Math.round((i * 255.0) / (nIdx - 1))
            }
        }
        if (typeof As === 'undefined') {
            As = new Uint8ClampedArray(nIdx).fill(64)
            As[0] = 0
        }
        for (let i = 0; i < nIdx - 1; i++) {
            const idxLo = Is[i]
            let idxHi = Is[i + 1]
            if (i === 0 && idxLo !== 0) {
                log.warn('colormap issue: indices expected to start with 0 not ', idxLo)
            }
            if (i === Is.length - 2 && idxHi !== 255) {
                log.warn('padding colormap: indices expected end with 255 not ', idxHi)
                idxHi = 255
            }
            const idxRng = idxHi - idxLo
            let k = idxLo * 4
            for (let j = idxLo; j <= idxHi; j++) {
                const f = (j - idxLo) / idxRng
                lut[k++] = Rs[i] + f * (Rs[i + 1] - Rs[i]) // Red
                lut[k++] = Gs[i] + f * (Gs[i + 1] - Gs[i]) // Green
                lut[k++] = Bs[i] + f * (Bs[i + 1] - Bs[i]) // Blue
                lut[k++] = As[i] + f * (As[i + 1] - As[i]) // Alpha
            }
        }
        if (this.gamma === 1.0) {
            return lut
        }
        for (let i = 0; i < 255 * 4; i++) {
            if (i % 4 === 3) {
                continue
            } // gamma changes RGB, not Alpha
            lut[i] = Math.pow(lut[i] / 255, 1 / this.gamma) * 255
        }
        return lut
    }
}

export const cmapper = new ColorTables()
