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

/**
 * Reads ITK MetaImage format (MHA/MHD), modifying the provided NVImage header
 * and returning the raw image data buffer.
 *
 * MHA files contain both header and image data in one file.
 * MHD files contain only the header, with image data in a separate file.
 *
 * Format specification: https://itk.org/Wiki/ITK/MetaIO/Documentation
 *
 * @param nvImage - The NVImage instance whose header will be modified.
 * @param buffer - ArrayBuffer containing the MHA/MHD file data.
 * @param pairedImgData - Optional paired image data for detached MHD headers.
 * @returns Promise resolving to ArrayBuffer containing the image data.
 * @throws Error if the file is too small or has unsupported data types.
 */
export async function readMHA(nvImage: NVImage, buffer: ArrayBuffer, pairedImgData: ArrayBuffer | null): Promise<ArrayBuffer> {
    const len = buffer.byteLength
    if (len < 20) {
        throw new Error('File too small to be VTK: bytes = ' + buffer.byteLength)
    }
    const bytes = new Uint8Array(buffer)
    let pos = 0
    function eol(c: number): boolean {
        return c === 10 || c === 13 // c is either a line feed character (10) or carriage return character (13)
    }
    function readStr(): string {
        while (pos < len && eol(bytes[pos])) {
            pos++
        } // Skip blank lines
        const startPos = pos
        while (pos < len && !eol(bytes[pos])) {
            pos++
        } // Forward until end of line
        if (pos - startPos < 2) {
            return ''
        }
        return new TextDecoder().decode(buffer.slice(startPos, pos))
    }
    let line = readStr() // 1st line: signature
    nvImage.hdr = new NIFTI1()
    const hdr = nvImage.hdr
    hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]
    hdr.dims = [1, 1, 1, 1, 1, 1, 1, 1]
    hdr.littleEndian = true
    let isGz = false
    let isDetached = false
    const mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1)
    const offset = vec3.fromValues(0, 0, 0)
    while (line !== '') {
        let items = line.split(' ')
        if (items.length > 2) {
            items = items.slice(2)
        }
        if (line.startsWith('BinaryDataByteOrderMSB') && items[0].includes('False')) {
            hdr.littleEndian = true
        }
        if (line.startsWith('BinaryDataByteOrderMSB') && items[0].includes('True')) {
            hdr.littleEndian = false
        }
        if (line.startsWith('CompressedData') && items[0].includes('True')) {
            isGz = true
        }
        if (line.startsWith('TransformMatrix')) {
            for (let d = 0; d < 9; d++) {
                mat33[d] = parseFloat(items[d])
            }
        }
        if (line.startsWith('Offset')) {
            for (let d = 0; d < Math.min(items.length, 3); d++) {
                offset[d] = parseFloat(items[d])
            }
        }
        // if (line.startsWith("AnatomicalOrientation")) //we can ignore, tested with Slicer3D converting NIfTIspace images
        if (line.startsWith('ElementSpacing')) {
            for (let d = 0; d < items.length; d++) {
                hdr.pixDims[d + 1] = parseFloat(items[d])
            }
        }
        if (line.startsWith('DimSize')) {
            hdr.dims[0] = items.length
            for (let d = 0; d < items.length; d++) {
                hdr.dims[d + 1] = parseInt(items[d])
            }
        }
        if (line.startsWith('ElementType')) {
            switch (items[0]) {
                case 'MET_UCHAR':
                    hdr.numBitsPerVoxel = 8
                    hdr.datatypeCode = NiiDataType.DT_UINT8
                    break
                case 'MET_CHAR':
                    hdr.numBitsPerVoxel = 8
                    hdr.datatypeCode = NiiDataType.DT_INT8
                    break
                case 'MET_SHORT':
                    hdr.numBitsPerVoxel = 16
                    hdr.datatypeCode = NiiDataType.DT_INT16
                    break
                case 'MET_USHORT':
                    hdr.numBitsPerVoxel = 16
                    hdr.datatypeCode = NiiDataType.DT_UINT16
                    break
                case 'MET_INT':
                    hdr.numBitsPerVoxel = 32
                    hdr.datatypeCode = NiiDataType.DT_INT32
                    break
                case 'MET_UINT':
                    hdr.numBitsPerVoxel = 32
                    hdr.datatypeCode = NiiDataType.DT_UINT32
                    break
                case 'MET_FLOAT':
                    hdr.numBitsPerVoxel = 32
                    hdr.datatypeCode = NiiDataType.DT_FLOAT32
                    break
                case 'MET_DOUBLE':
                    hdr.numBitsPerVoxel = 64
                    hdr.datatypeCode = NiiDataType.DT_FLOAT64
                    break
                default:
                    throw new Error('Unsupported MHA data type: ' + items[0])
            }
        }
        if (line.startsWith('ObjectType') && !items[0].includes('Image')) {
            log.warn('Only able to read ObjectType = Image, not ' + line)
        }
        if (line.startsWith('ElementDataFile')) {
            if (items[0] !== 'LOCAL') {
                isDetached = true
            }
            break
        }
        line = readStr()
    }
    const mmMat = mat3.fromValues(hdr.pixDims[1], 0, 0, 0, hdr.pixDims[2], 0, 0, 0, hdr.pixDims[3])
    mat3.multiply(mat33, mat33, mmMat)
    hdr.affine = [
        [-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]
    ]
    while (bytes[pos] === 10) {
        pos++
    }
    hdr.vox_offset = pos
    if (isDetached && pairedImgData) {
        if (isGz) {
            return await NVUtilities.decompressToBuffer(new Uint8Array(pairedImgData.slice(0)))
        }
        return pairedImgData.slice(0)
    }
    if (isGz) {
        return await NVUtilities.decompressToBuffer(new Uint8Array(buffer.slice(hdr.vox_offset)))
    }
    return buffer.slice(hdr.vox_offset)
}
