// Copyright (c) 2025 lws
// Image decoding library for MoonBit - Main entry point

//-----------------------------------------------------------------------------
// Format Detection
//-----------------------------------------------------------------------------

///|
/// Detect the image format from magic bytes (file signature)
/// PNG and QOI have distinctive signatures, BMP has "BM", TGA uses heuristic + footer
pub fn detect_format(data : Bytes) -> ImageFormat? {
  if data.length() < 8 {
    return None
  }

  // PNG: \x89 P N G \r \n \x1A \n (8-byte signature)
  if data[0] == b'\x89' &&
    data[1] == b'P' &&
    data[2] == b'N' &&
    data[3] == b'G' &&
    data[4] == b'\r' &&
    data[5] == b'\n' &&
    data[6] == b'\x1A' &&
    data[7] == b'\n' {
    return Some(ImageFormat::PNG)
  }

  // QOI: q o i f (4-byte magic)
  if data[0] == b'q' && data[1] == b'o' && data[2] == b'i' && data[3] == b'f' {
    return Some(ImageFormat::QOI)
  }

  // GIF: G I F 8 7 a or G I F 8 9 a (6-byte signature)
  if data[0] == b'G' &&
    data[1] == b'I' &&
    data[2] == b'F' &&
    data[3] == b'8' &&
    (data[4] == b'7' || data[4] == b'9') &&
    data[5] == b'a' {
    return Some(ImageFormat::GIF)
  }

  // BMP: B M (2-byte signature)
  if data[0] == b'B' && data[1] == b'M' {
    return Some(ImageFormat::BMP)
  }

  // TGA detection: check for type 2 footer first (most reliable),
  // then fall back to header heuristic
  if detect_tga(data) {
    return Some(ImageFormat::TGA)
  }

  // JPEG: FF D8 FF marker (check this last since FF D8 can appear in other binary data)
  if data.length() >= 3 && data[0] == b'\xFF' && data[1] == b'\xD8' {
    return Some(ImageFormat::JPEG)
  }

  None
}

///|
/// Try to detect TGA by footer or header heuristic
fn detect_tga(data : Bytes) -> Bool {
  if data.length() < 18 {
    return false
  }

  // Method 1: Check for TGA version 2 footer at end of file
  // Footer signature: "TRUEVISION-XFILE." at position len-18
  let footer_sig = "TRUEVISION-XFILE."
  let len = data.length()
  if len >= 26 {
    let footer_start = len - 18
    let mut matches = true
    for i = 0; i < 18; i = i + 1 {
      if data[footer_start + i] != footer_sig[i].to_byte() {
        matches = false
        break
      }
    }
    if matches {
      return true
    }
  }

  // Method 2: Header heuristic for type 1 (no footer) TGA files
  let id_length = data[0].to_int()
  let color_map_type = data[1].to_int()
  let image_type = data[2].to_int()

  // Valid TGA image types (2=uncompressed true-color, 3=grayscale, 10=RLE true-color, 11=RLE grayscale)
  let valid_type = image_type == 2 ||
    image_type == 3 ||
    image_type == 10 ||
    image_type == 11
  // Valid color map types (0=none, 1=present)
  let valid_cmap = color_map_type == 0 || color_map_type == 1

  if !valid_type || !valid_cmap {
    return false
  }

  // Validate dimensions
  let width = data[12].to_int() | (data[13].to_int() << 8)
  let height = data[14].to_int() | (data[15].to_int() << 8)
  if width == 0 || height == 0 {
    return false
  }

  let pixel_depth = data[16].to_int()
  let valid_depth = pixel_depth == 8 ||
    pixel_depth == 16 ||
    pixel_depth == 24 ||
    pixel_depth == 32

  id_length >= 0 && id_length < 256 && valid_depth
}

//-----------------------------------------------------------------------------
// Unified Decode API
//-----------------------------------------------------------------------------

///|
/// Auto-detect format and decode an image from raw bytes
pub fn decode(data : Bytes) -> Image raise Failure {
  match detect_format(data) {
    None => raise Failure::Failure("Unsupported or unknown image format")
    Some(format) => decode_by_format(data, format)
  }
}

///|
/// Decode an image with a known format
pub fn decode_by_format(
  data : Bytes,
  format : ImageFormat,
) -> Image raise Failure {
  match format {
    ImageFormat::BMP => decode_bmp(data)
    ImageFormat::QOI => decode_qoi(data)
    ImageFormat::TGA => decode_tga(data)
    ImageFormat::PNG => decode_png(data)
    ImageFormat::GIF => decode_gif(data)
    ImageFormat::JPEG => decode_jpeg(data)
  }
}

//-----------------------------------------------------------------------------
// Header-only dimension reading (fast, no pixel decoding)
//-----------------------------------------------------------------------------

///|
/// Get image dimensions from a file header without decoding pixels
/// Much faster than decode() for inspecting image metadata
pub fn image_dimensions(data : Bytes) -> (Int, Int, ImageFormat) raise Failure {
  match detect_format(data) {
    None => raise Failure::Failure("Unsupported or unknown image format")
    Some(format) =>
      match format {
        ImageFormat::BMP => bmp_dimensions(data)
        ImageFormat::QOI => qoi_dimensions(data)
        ImageFormat::TGA => tga_dimensions(data)
        ImageFormat::PNG => png_dimensions(data)
        ImageFormat::GIF => gif_dimensions(data)
        ImageFormat::JPEG => {
          let (w, h) = jpeg_dimensions(data)
          (w, h, ImageFormat::JPEG)
        }
      }
  }
}

///|
/// Read BMP dimensions from header (fast, no pixel decode)
fn bmp_dimensions(data : Bytes) -> (Int, Int, ImageFormat) raise Failure {
  if data.length() < 26 {
    raise Failure::Failure("BMP: file too small for header reading")
  }
  let width_raw = read_u32_le(data, 18)
  let height_raw = read_u32_le(data, 22)
  let height = if height_raw < 0 { -height_raw } else { height_raw }
  (width_raw, height, ImageFormat::BMP)
}

///|
/// Read QOI dimensions from header
fn qoi_dimensions(data : Bytes) -> (Int, Int, ImageFormat) raise Failure {
  if data.length() < 14 {
    raise Failure::Failure("QOI: file too small for header reading")
  }
  let width = read_u32_be(data, 4)
  let height = read_u32_be(data, 8)
  (width, height, ImageFormat::QOI)
}

///|
/// Read TGA dimensions from header
fn tga_dimensions(data : Bytes) -> (Int, Int, ImageFormat) raise Failure {
  if data.length() < 18 {
    raise Failure::Failure("TGA: file too small for header reading")
  }
  let width = read_u16_le(data, 12)
  let height = read_u16_le(data, 14)
  (width, height, ImageFormat::TGA)
}

///|
/// Read GIF dimensions from header
fn gif_dimensions(data : Bytes) -> (Int, Int, ImageFormat) raise Failure {
  if data.length() < 10 {
    raise Failure::Failure("GIF: file too small for header reading")
  }
  let width = read_u16_le(data, 6)
  let height = read_u16_le(data, 8)
  (width, height, ImageFormat::GIF)
}

///|
/// Read PNG dimensions from IHDR chunk (fast, only parses the first chunk)
fn png_dimensions(data : Bytes) -> (Int, Int, ImageFormat) raise Failure {
  // PNG: 8-byte signature + 4-byte length + 4-byte "IHDR" + 13-byte IHDR data
  if data.length() < 33 {
    raise Failure::Failure("PNG: file too small for IHDR reading")
  }
  // Skip signature (8), read length (4), verify "IHDR" (4)
  if data[12] != b'I' ||
    data[13] != b'H' ||
    data[14] != b'D' ||
    data[15] != b'R' {
    raise Failure::Failure("PNG: first chunk is not IHDR")
  }
  let width = read_u32_be(data, 16)
  let height = read_u32_be(data, 20)
  (width, height, ImageFormat::PNG)
}

//-----------------------------------------------------------------------------
// Convenience Functions
//-----------------------------------------------------------------------------

///|
/// Check if data appears to be a supported image format
pub fn is_supported_format(data : Bytes) -> Bool {
  match detect_format(data) {
    None => false
    Some(_) => true
  }
}

///|
/// Get a human-readable string for a pixel format
pub fn pixel_format_name(format : PixelFormat) -> String {
  match format {
    PixelFormat::Gray8 => "Gray8"
    PixelFormat::GrayA8 => "GrayA8"
    PixelFormat::RGB8 => "RGB8"
    PixelFormat::RGBA8 => "RGBA8"
  }
}

///|
/// Get a human-readable string for an image format
pub fn image_format_name(format : ImageFormat) -> String {
  match format {
    ImageFormat::BMP => "BMP"
    ImageFormat::QOI => "QOI"
    ImageFormat::TGA => "TGA"
    ImageFormat::PNG => "PNG"
    ImageFormat::GIF => "GIF"
    ImageFormat::JPEG => "JPEG"
  }
}

//-----------------------------------------------------------------------------
// Unified Encode API
//-----------------------------------------------------------------------------

///|
/// Encode an image to the specified format
/// Supports BMP, QOI encoding. TGA encoding planned.
pub fn encode(img : Image, format : ImageFormat) -> Bytes raise Failure {
  match format {
    ImageFormat::BMP => encode_bmp(img)
    ImageFormat::QOI => encode_qoi(img)
    _ =>
      raise Failure::Failure(
        "Encoding not yet supported for " + image_format_name(format),
      )
  }
}