import { NIFTI1 } from 'nifti-reader-js'
import { mat3, mat4, vec3 } from 'gl-matrix'
import { log } from '@/logger'
import { NVUtilities } from '@/nvutilities'
import type { NVImage } from '@/nvimage'
import { NiiDataType } from '@/nvimage/utils'

/**
 * Reads NRRD/NHDR format image, modifying the provided NVImage header
 * and returning the raw image data buffer.
 * @param nvImage - The NVImage instance whose header will be modified.
 * @param dataBuffer - ArrayBuffer containing the NRRD header or full file.
 * @param pairedImgData - Optional ArrayBuffer for detached data file (used by NHDR).
 * @returns Promise resolving to the imgRaw ArrayBuffer or null on critical error.
 */
export async function readNrrd(nvImage: NVImage, dataBuffer: ArrayBuffer, pairedImgData: ArrayBuffer | null = null): Promise<ArrayBuffer | null> {
    if (!nvImage.hdr) {
        log.warn('readNrrd called before nvImage.hdr was initialized. Creating default.')
        nvImage.hdr = new NIFTI1()
    }
    const hdr = nvImage.hdr // Use nvImage.hdr directly
    hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]
    hdr.dims = [0, 0, 0, 0, 0, 0, 0, 0]
    const len = dataBuffer.byteLength
    let txt: string | null = null
    const bytes = new Uint8Array(dataBuffer)

    for (let i = 1; i < len; i++) {
        if (bytes[i - 1] === 10 && bytes[i] === 10) {
            const v = dataBuffer.slice(0, i - 1)
            txt = new TextDecoder().decode(v)
            hdr.vox_offset = i + 1 // Set based on header end position
            break
        }
    }

    if (txt === null) {
        log.error('readNrrd: could not extract txt')
        return null
    }

    const lines = txt.split('\n')
    if (!lines[0].startsWith('NRRD')) {
        log.error('Invalid NRRD image (magic signature missing)')
        return null
    }

    const n = lines.length
    let isGz = false
    let isMicron = false
    let isDetached = false
    const mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1)
    const offset = vec3.fromValues(0, 0, 0)
    let rot33 = mat3.create() // Initialize space correction matrix

    for (let i = 1; i < n; i++) {
        let str = lines[i]
        if (str.length === 0 || str[0] === '#') {
            if (str.startsWith('#')) {
                continue
            }
            if (str.trim().length === 0) {
                continue
            }
        }
        str = str.toLowerCase()
        const items = str.split(':')
        if (items.length < 2) {
            continue
        }
        const key = items[0].trim()
        let value = items[1].trim()

        value = value.replaceAll(')', ' ')
        value = value.replaceAll('(', ' ')
        value = value.trim()

        switch (key) {
            case 'data file':
                isDetached = true
                break
            case 'encoding':
                if (value.includes('raw')) {
                    isGz = false
                } else if (value.includes('gz')) {
                    isGz = true
                } else {
                    log.error(`Unsupported NRRD encoding : ${value}`)
                    return null
                }
                break
            case 'type':
                switch (value) {
                    case 'uchar':
                    case 'unsigned char':
                    case 'uint8':
                    case 'uint8_t':
                        hdr.numBitsPerVoxel = 8
                        hdr.datatypeCode = NiiDataType.DT_UINT8
                        break
                    case 'signed char':
                    case 'int8':
                    case 'int8_t':
                        hdr.numBitsPerVoxel = 8
                        hdr.datatypeCode = NiiDataType.DT_INT8
                        break
                    case 'short':
                    case 'short int':
                    case 'signed short':
                    case 'signed short int':
                    case 'int16':
                    case 'int16_t':
                        hdr.numBitsPerVoxel = 16
                        hdr.datatypeCode = NiiDataType.DT_INT16
                        break
                    case 'ushort':
                    case 'unsigned short':
                    case 'unsigned short int':
                    case 'uint16':
                    case 'uint16_t':
                        hdr.numBitsPerVoxel = 16
                        hdr.datatypeCode = NiiDataType.DT_UINT16
                        break
                    case 'int':
                    case 'signed int':
                    case 'int32':
                    case 'int32_t':
                        hdr.numBitsPerVoxel = 32
                        hdr.datatypeCode = NiiDataType.DT_INT32
                        break
                    case 'uint':
                    case 'unsigned int':
                    case 'uint32':
                    case 'uint32_t':
                        hdr.numBitsPerVoxel = 32
                        hdr.datatypeCode = NiiDataType.DT_UINT32
                        break
                    case 'float':
                        hdr.numBitsPerVoxel = 32
                        hdr.datatypeCode = NiiDataType.DT_FLOAT32
                        break
                    case 'double':
                        hdr.numBitsPerVoxel = 64
                        hdr.datatypeCode = NiiDataType.DT_FLOAT64
                        break
                    default:
                        log.error('Unsupported NRRD data type: ' + value)
                        return null
                }
                break
            case 'spacings':
                {
                    const values = value.split(/[ ,]+/)
                    for (let d = 0; d < values.length; d++) {
                        hdr.pixDims[d + 1] = parseFloat(values[d])
                    }
                }
                break
            case 'sizes':
                {
                    const dims = value.split(/[ ,]+/)
                    hdr.dims[0] = dims.length
                    for (let d = 0; d < dims.length; d++) {
                        hdr.dims[d + 1] = parseInt(dims[d])
                    }
                }
                break
            case 'endian':
                if (value.includes('little')) {
                    hdr.littleEndian = true
                } else if (value.includes('big')) {
                    hdr.littleEndian = false
                }
                break
            case 'space directions':
                {
                    const vs = value.split(/[ ,]+/)
                    if (vs.length === 9) {
                        for (let d = 0; d < 9; d++) {
                            mat33[d] = parseFloat(vs[d])
                        }
                    }
                }
                break
            case 'space origin':
                {
                    const ts = value.split(/[ ,]+/)
                    if (ts.length === 3) {
                        offset[0] = parseFloat(ts[0])
                        offset[1] = parseFloat(ts[1])
                        offset[2] = parseFloat(ts[2])
                    }
                }
                break
            case 'space units':
                if (value.includes('microns')) {
                    isMicron = true
                }
                break
            case 'space':
                if (value.includes('right-anterior-superior') || value.includes('ras')) {
                    rot33 = mat3.fromValues(1, 0, 0, 0, 1, 0, 0, 0, 1)
                } else if (value.includes('left-anterior-superior') || value.includes('las')) {
                    rot33 = mat3.fromValues(-1, 0, 0, 0, 1, 0, 0, 0, 1)
                } else if (value.includes('left-posterior-superior') || value.includes('lps')) {
                    rot33 = mat3.fromValues(-1, 0, 0, 0, -1, 0, 0, 0, 1)
                } else {
                    log.warn('Unsupported NRRD space value:', value)
                }
                break
            default:
                log.warn('Unknown:', key)
                break
        }
    }

    if (!isNaN(mat33[0])) {
        hdr.sform_code = 2
        if (isMicron) {
            // @ts-expect-error FIXME: converting mat3 to mat4
            mat4.multiplyScalar(mat33, mat33, 0.001)
            offset[0] *= 0.001
            offset[1] *= 0.001
            offset[2] *= 0.001
        }
        if (rot33[0] < 0) {
            offset[0] = -offset[0]
        }
        if (rot33[4] < 0) {
            offset[1] = -offset[1]
        }
        if (rot33[8] < 0) {
            offset[2] = -offset[2]
        }
        mat3.multiply(mat33, rot33, mat33)

        const mat = mat4.fromValues(mat33[0], mat33[3], mat33[6], offset[0], mat33[1], mat33[4], mat33[7], offset[1], mat33[2], mat33[5], mat33[8], offset[2], 0, 0, 0, 1)

        // Ensure vox2mm function is accessible via nvImage
        if (!nvImage.vox2mm) {
            return null
        }
        const mm000 = nvImage.vox2mm([0, 0, 0], mat)
        const mm100 = nvImage.vox2mm([1, 0, 0], mat)
        vec3.subtract(mm100, mm100, mm000)
        const mm010 = nvImage.vox2mm([0, 1, 0], mat)
        vec3.subtract(mm010, mm010, mm000)
        const mm001 = nvImage.vox2mm([0, 0, 1], mat)
        vec3.subtract(mm001, mm001, mm000)
        hdr.pixDims[0] = 1
        hdr.pixDims[1] = vec3.length(mm100)
        hdr.pixDims[2] = vec3.length(mm010)
        hdr.pixDims[3] = vec3.length(mm001)

        hdr.affine = [
            [mat[0], mat[1], mat[2], mat[3]],
            [mat[4], mat[5], mat[6], mat[7]],
            [mat[8], mat[9], mat[10], mat[11]],
            [0, 0, 0, 1]
        ]
    }
    let imgRaw: ArrayBuffer | null = null // Use null for error case

    // Data source depends on whether header was detached
    const sourceBuffer = isDetached ? pairedImgData : dataBuffer
    // Offset where data starts within the sourceBuffer
    const sourceOffset = isDetached ? 0 : hdr.vox_offset // Use hdr.vox_offset set during header parsing

    if (isDetached && !sourceBuffer) {
        log.warn('Missing data: NRRD header describes detached data file but only one URL provided')
        return null
    }

    if (!sourceBuffer || sourceOffset >= sourceBuffer.byteLength) {
        log.error(`NRRD data offset (${sourceOffset}) invalid for buffer length (${sourceBuffer?.byteLength ?? 0})`)
        return null
    }

    // Slice the data section
    let dataSection = sourceBuffer.slice(sourceOffset)

    // Decompress if necessary
    if (isGz) {
        try {
            log.debug('Decompressing NRRD data...')
            dataSection = await NVUtilities.decompressToBuffer(new Uint8Array(dataSection))
            log.debug('Decompression complete.')
        } catch (err) {
            log.error('Failed to decompress NRRD data.', err)
            return null
        }
    }

    const nBytesPerVoxel = hdr.numBitsPerVoxel / 8
    const nVoxels = hdr.dims.slice(1, hdr.dims[0] + 1).reduce((acc, dim) => acc * Math.max(1, dim), 1)
    const expectedBytes = nVoxels * nBytesPerVoxel
    if (dataSection.byteLength < expectedBytes) {
        log.error(`NRRD image data size mismatch: expected ${expectedBytes}, found ${dataSection.byteLength}`)
        return null
    } else if (dataSection.byteLength > expectedBytes) {
        log.warn(`NRRD has extra ${dataSection.byteLength - expectedBytes} bytes after expected image data. Truncating.`)
        dataSection = dataSection.slice(0, expectedBytes)
    }

    imgRaw = dataSection // Assign the final buffer

    // Ensure header has essential NIFTI fields if missing defaults
    if (!hdr.datatypeCode) {
        log.error('NRRD parsing failed to set datatypeCode.')
        return null
    }
    if (!hdr.numBitsPerVoxel) {
        log.error('NRRD parsing failed to set numBitsPerVoxel.')
        return null
    }

    return imgRaw // Return the image data buffer
}
