import { log } from '../logger.js'
import { NiiDataType } from './utils.js'
import type { NVImage, TypedVoxelArray } from './index.js'

/**
 * Returns voxel intensity at specified native coordinates. Handles reorientation if necessary.
 * @param nvImage - The NVImage instance
 * @param x - Native X coordinate (0-indexed)
 * @param y - Native Y coordinate (0-indexed)
 * @param z - Native Z coordinate (0-indexed)
 * @param frame4D - 4D frame index (0-indexed)
 * @param isReadImaginary - Flag to read from imaginary data array if complex
 * @returns Scaled voxel intensity
 */
export function getValue(
  nvImage: NVImage,
  x: number,
  y: number,
  z: number,
  frame4D = 0,
  isReadImaginary = false
): number {
  if (!nvImage.hdr) {
    throw new Error('getValue: NVImage header is not defined.')
  }
  // Image data existence check depends on isReadImaginary
  if (!isReadImaginary && !nvImage.img) {
    throw new Error('getValue: NVImage image data is not defined.')
  }
  if (isReadImaginary && !nvImage.imaginary) {
    // If requesting imaginary but it doesn't exist, return 0 or throw? Let's return 0 for now.
    log.warn('getValue: Attempted to read imaginary data, but none exists.')
    return 0
  }

  const nx = nvImage.hdr.dims[1]
  const ny = nvImage.hdr.dims[2]
  const nz = nvImage.hdr.dims[3]

  // Input coordinates (x, y, z) are assumed to be in the image's NATIVE space based on original function context.
  // NO reorientation logic needed here if inputs are native.
  // If coordinates were meant to be RAS, reorientation would be needed *before* this function or added here.
  // Sticking to the original implementation's apparent assumption of native coordinates.

  // Clamp coordinates to valid native range
  x = Math.max(0, Math.min(Math.round(x), nx - 1))
  y = Math.max(0, Math.min(Math.round(y), ny - 1))
  z = Math.max(0, Math.min(Math.round(z), nz - 1))
  frame4D = Math.max(0, frame4D) // Basic frame check

  let vx = x + y * nx + z * nx * ny // Native voxel index within a 3D volume

  // Handle RGB(A) data - calculate luminance
  if (nvImage.hdr.datatypeCode === NiiDataType.DT_RGBA32) {
    if (!nvImage.img) {
      return 0
    } // Guard added
    vx *= 4 // 4 bytes per voxel
    // Check bounds for RGBA index access
    if (vx + 2 >= nvImage.img.length) {
      log.warn(`getValue: Calculated index ${vx} out of bounds for RGBA data.`)
      return 0 // Or throw? Return 0 for safety.
    }
    // convert rgb to luminance Y = 0.2126 R + 0.7152 G + 0.0722 B (Rec. 709)
    const lum = nvImage.img[vx] * 0.2126 + nvImage.img[vx + 1] * 0.7152 + nvImage.img[vx + 2] * 0.0722
    // RGB is usually UINT8, doesn't typically use scl_slope/inter
    return Math.round(lum)
  }
  if (nvImage.hdr.datatypeCode === NiiDataType.DT_RGB24) {
    if (!nvImage.img) {
      return 0
    } // Guard added
    vx *= 3 // 3 bytes per voxel
    if (vx + 2 >= nvImage.img.length) {
      log.warn(`getValue: Calculated index ${vx} out of bounds for RGB data.`)
      return 0
    }
    const lum = nvImage.img[vx] * 0.2126 + nvImage.img[vx + 1] * 0.7152 + nvImage.img[vx + 2] * 0.0722
    return Math.round(lum)
  }

  // Calculate offset for 4D frame
  const nVox3D = nx * ny * nz
  const volOffset = frame4D * nVox3D
  const finalVxIndex = vx + volOffset

  // Select the correct data array
  const dataArray = isReadImaginary ? nvImage.imaginary! : nvImage.img!

  // Check final index bounds
  if (finalVxIndex < 0 || finalVxIndex >= dataArray.length) {
    // log.warn(`getValue: Calculated index ${finalVxIndex} is out of bounds [0..${dataArray.length-1}]. Frame ${frame4D}?`);
    // Return 0 for out-of-bounds access, consistent with how shaders might clamp.
    return 0
  }

  const rawValue = dataArray[finalVxIndex]

  // Apply scaling slope and intercept
  // Use default 1.0 slope if hdr value is 0 or NaN, default 0.0 intercept if NaN
  const slope = isNaN(nvImage.hdr.scl_slope) || nvImage.hdr.scl_slope === 0 ? 1.0 : nvImage.hdr.scl_slope
  const inter = isNaN(nvImage.hdr.scl_inter) ? 0.0 : nvImage.hdr.scl_inter

  return slope * rawValue + inter
}

/**
 * Reads a 3D slab of voxels from a volume, specified in RAS coordinates.
 * @param nvImage - The NVImage instance
 * @param voxStartRAS - First row, column, slice (RAS order, 0-indexed) for selection
 * @param voxEndRAS - Final row, column, slice (RAS order, 0-indexed) for selection
 * @param dataType - Output array type: 'same', 'uint8', 'float32', 'scaled', 'normalized', 'windowed'
 * @returns Tuple: [TypedVoxelArray, slabDimensions]
 */
export function getVolumeData(
  nvImage: NVImage,
  voxStartRAS: number[] = [-1, 0, 0],
  voxEndRAS: number[] = [0, 0, 0],
  dataType = 'same'
): [TypedVoxelArray, number[]] {
  const defaultResult: [TypedVoxelArray, number[]] = [new Uint8Array(), [0, 0, 0]]

  if (!nvImage.hdr || !nvImage.img || !nvImage.dimsRAS || !nvImage.img2RASstep || !nvImage.img2RASstart) {
    log.error('getVolumeData: Missing required NVImage properties (hdr, img, dimsRAS, img2RASstep/start).')
    return defaultResult
  }
  // Ensure input arrays have 3 elements
  voxStartRAS = voxStartRAS.slice(0, 3)
  voxEndRAS = voxEndRAS.slice(0, 3)

  if (Math.min(...voxStartRAS) < 0 || Math.min(...voxEndRAS) < 0) {
    log.warn('getVolumeData: Invalid start or end coordinates provided.')
    return defaultResult
  }

  const dimsRAS = nvImage.dimsRAS.slice(1, 4) // Get RAS dimensions [nx, ny, nz]

  // Clamp coordinates to valid RAS range and ensure start <= end
  for (let i = 0; i < 3; i++) {
    voxStartRAS[i] = Math.max(0, Math.min(Math.round(voxStartRAS[i]), dimsRAS[i] - 1))
    voxEndRAS[i] = Math.max(0, Math.min(Math.round(voxEndRAS[i]), dimsRAS[i] - 1))
    if (voxEndRAS[i] < voxStartRAS[i]) {
      const tmp = voxEndRAS[i]
      voxEndRAS[i] = voxStartRAS[i]
      voxStartRAS[i] = tmp
    }
  }

  const slabDims = [
    voxEndRAS[0] - voxStartRAS[0] + 1,
    voxEndRAS[1] - voxStartRAS[1] + 1,
    voxEndRAS[2] - voxStartRAS[2] + 1
  ]
  const slabNVox = slabDims[0] * slabDims[1] * slabDims[2]

  if (slabNVox <= 0) {
    log.warn('getVolumeData: Calculated slab size is zero or negative.')
    return defaultResult
  }

  let OutputArrayConstructor: new (length: number) => TypedVoxelArray = nvImage.img.constructor as new (
    length: number
  ) => TypedVoxelArray // Default to same as input

  if (dataType === 'uint8') {
    OutputArrayConstructor = Uint8Array
  } else if (dataType === 'int16') {
    OutputArrayConstructor = Int16Array
  } else if (dataType === 'uint16') {
    OutputArrayConstructor = Uint16Array
  } else if (dataType === 'float32' || dataType === 'scaled' || dataType === 'normalized' || dataType === 'windowed') {
    OutputArrayConstructor = Float32Array
  } else if (dataType === 'float64') {
    OutputArrayConstructor = Float64Array
  } else if (dataType !== 'same') {
    log.warn(`getVolumeData: Unsupported dataType '${dataType}'. Using 'same'.`)
  }

  // Create the output array
  let outputImg: TypedVoxelArray
  try {
    outputImg = new OutputArrayConstructor(slabNVox)
  } catch (e) {
    log.error(`getVolumeData: Failed to create output array for dataType '${dataType}'.`, e)
    return defaultResult
  }

  // Get transformation parameters
  const step = nvImage.img2RASstep
  const start = nvImage.img2RASstart
  const sourceImg = nvImage.img // Source data in native orientation

  let outputIndex = 0
  // Iterate through the requested RAS slab dimensions
  for (let rz = voxStartRAS[2]; rz <= voxEndRAS[2]; rz++) {
    const zi = start[2] + rz * step[2] // Native offset component for RAS Z
    for (let ry = voxStartRAS[1]; ry <= voxEndRAS[1]; ry++) {
      const yi = start[1] + ry * step[1] // Native offset component for RAS Y
      for (let rx = voxStartRAS[0]; rx <= voxEndRAS[0]; rx++) {
        const xi = start[0] + rx * step[0] // Native offset component for RAS X
        const nativeIndex = xi + yi + zi // Final index in the native source buffer

        let value = 0
        // Safely read from source image
        if (nativeIndex >= 0 && nativeIndex < sourceImg.length) {
          value = sourceImg[nativeIndex]
        } else {
          // log.warn(`getVolumeData: Calculated native index ${nativeIndex} out of bounds during read.`);
          // Use 0 for out-of-bounds reads
        }

        // Store the raw value in the output array
        outputImg[outputIndex++] = value
      }
    }
  }

  // Apply post-processing based on dataType AFTER extracting raw values
  const slope = isNaN(nvImage.hdr.scl_slope) || nvImage.hdr.scl_slope === 0 ? 1.0 : nvImage.hdr.scl_slope
  const inter = isNaN(nvImage.hdr.scl_inter) ? 0.0 : nvImage.hdr.scl_inter

  if (dataType === 'scaled' || dataType === 'normalized' || dataType === 'windowed') {
    // Ensure output is Float32 if scaling is requested but wasn't the original type
    if (!(outputImg instanceof Float32Array)) {
      log.warn(`getVolumeData: Converting output to Float32 for scaling type '${dataType}'.`)
      outputImg = Float32Array.from(outputImg)
    }
    for (let i = 0; i < outputImg.length; i++) {
      outputImg[i] = outputImg[i] * slope + inter
    }
  }

  if (dataType === 'normalized' || dataType === 'windowed') {
    let minVal = nvImage.cal_min
    let maxVal = nvImage.cal_max

    if (dataType === 'normalized') {
      minVal = nvImage.global_min
      maxVal = nvImage.global_max
    }

    const range = maxVal - minVal
    const scale = range === 0 ? 0 : 1 / range

    for (let i = 0; i < outputImg.length; i++) {
      outputImg[i] = (outputImg[i] - minVal) * scale
      outputImg[i] = Math.max(0, Math.min(outputImg[i], 1))
    }
  }

  return [outputImg, slabDims]
}

/**
 * Writes a 3D slab of voxels into a volume, specified in RAS coordinates.
 * @param nvImage - The NVImage instance to modify
 * @param voxStartRAS - First row, column, slice (RAS order, 0-indexed) for selection
 * @param voxEndRAS - Final row, column, slice (RAS order, 0-indexed) for selection
 * @param slabData - Array of voxel values (TypedVoxelArray) matching slab dimensions
 */
export function setVolumeData(
  nvImage: NVImage,
  voxStartRAS: number[] = [-1, 0, 0],
  voxEndRAS: number[] = [0, 0, 0],
  slabData: TypedVoxelArray = new Uint8Array()
): void {
  if (!nvImage.hdr || !nvImage.img || !nvImage.dimsRAS || !nvImage.img2RASstep || !nvImage.img2RASstart) {
    log.error('setVolumeData: Missing required NVImage properties (hdr, img, dimsRAS, img2RASstep/start).')
    return
  }

  if (slabData.length < 1) {
    log.warn('setVolumeData: Input slabData is empty.')
    return
  }
  // Ensure input arrays have 3 elements
  voxStartRAS = voxStartRAS.slice(0, 3)
  voxEndRAS = voxEndRAS.slice(0, 3)

  if (Math.min(...voxStartRAS) < 0 || Math.min(...voxEndRAS) < 0) {
    log.warn('setVolumeData: Invalid start or end coordinates provided.')
    return
  }

  const dimsRAS = nvImage.dimsRAS.slice(1, 4) // Get RAS dimensions [nx, ny, nz]

  // Clamp coordinates to valid RAS range and ensure start <= end
  for (let i = 0; i < 3; i++) {
    voxStartRAS[i] = Math.max(0, Math.min(Math.round(voxStartRAS[i]), dimsRAS[i] - 1))
    voxEndRAS[i] = Math.max(0, Math.min(Math.round(voxEndRAS[i]), dimsRAS[i] - 1))
    if (voxEndRAS[i] < voxStartRAS[i]) {
      const tmp = voxEndRAS[i]
      voxEndRAS[i] = voxStartRAS[i]
      voxStartRAS[i] = tmp
    }
  }

  const slabDims = [
    voxEndRAS[0] - voxStartRAS[0] + 1,
    voxEndRAS[1] - voxStartRAS[1] + 1,
    voxEndRAS[2] - voxStartRAS[2] + 1
  ]
  const slabNVox = slabDims[0] * slabDims[1] * slabDims[2]

  if (slabNVox <= 0) {
    log.warn('setVolumeData: Calculated slab size is zero or negative.')
    return
  }

  if (slabData.length < slabNVox) {
    log.error(
      `setVolumeData: Input slabData length (${slabData.length}) is less than the calculated slab size (${slabNVox}).`
    )
    return
  }

  // Get transformation parameters
  const step = nvImage.img2RASstep
  const start = nvImage.img2RASstart
  const targetImg = nvImage.img // Target data in native orientation

  let sourceIndex = 0
  // Iterate through the requested RAS slab dimensions
  for (let rz = voxStartRAS[2]; rz <= voxEndRAS[2]; rz++) {
    const zi = start[2] + rz * step[2] // Native offset component for RAS Z
    for (let ry = voxStartRAS[1]; ry <= voxEndRAS[1]; ry++) {
      const yi = start[1] + ry * step[1] // Native offset component for RAS Y
      for (let rx = voxStartRAS[0]; rx <= voxEndRAS[0]; rx++) {
        const xi = start[0] + rx * step[0] // Native offset component for RAS X
        const nativeIndex = xi + yi + zi // Final index in the native target buffer

        // Safely write into target image if index is valid
        if (nativeIndex >= 0 && nativeIndex < targetImg.length) {
          // Potential type mismatch here! The input `slabData` might be Float32
          // while `targetImg` might be Uint8. Direct assignment might truncate.
          // Consider adding type checks or conversion logic if needed based on targetImg type.
          // For now, direct assignment assumes compatible types or acceptable truncation.
          targetImg[nativeIndex] = slabData[sourceIndex]
        } else {
          // log.warn(`setVolumeData: Calculated native index ${nativeIndex} out of bounds during write.`);
        }
        sourceIndex++
      }
    }
  }
  // Note: This function does NOT handle inverse scaling (converting scaled slabData back to raw).
  // The input slabData is assumed to be in the correct raw data type for the target nvImage.img.
}
