// 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? {
  // Each magic check is responsible for its own minimum length; an
  // outer `if length < 8` would falsely reject 6-byte GIF / 4-byte QOI
  // / 2-byte BMP / 3-byte JPEG signatures.

  // PNG: \x89 P N G \r \n \x1A \n (8-byte signature)
  if data.length() >= 8 &&
    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.length() >= 4 &&
    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.length() >= 6 &&
    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.length() >= 2 && 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 (requires >= 18 bytes).
  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)
  }

  // TIFF: byte-order mark "II" (little) or "MM" (big) + magic 42.
  // Little-endian 42 stores as 0x2A 0x00; big-endian 42 as 0x00 0x2A.
  if data.length() >= 4 {
    if data[0] == b'I' &&
      data[1] == b'I' &&
      data[2] == b'\x2A' &&
      data[3] == b'\x00' {
      return Some(ImageFormat::TIFF)
    }
    if data[0] == b'M' &&
      data[1] == b'M' &&
      data[2] == b'\x00' &&
      data[3] == b'\x2A' {
      return Some(ImageFormat::TIFF)
    }
  }

  // ICO: reserved(2)=0, type(2)=1 (icon), count(2) >= 1.
  // The reserved-zero prefix is too generic to use alone, so we
  // require the type=1 marker AND a non-zero entry count.
  if data.length() >= 6 &&
    data[0] == b'\x00' &&
    data[1] == b'\x00' &&
    data[2] == b'\x01' &&
    data[3] == b'\x00' &&
    data[4] >= b'\x01' {
    return Some(ImageFormat::ICO)
  }

  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
//
// Public decode / encode entry points raise the structured `DecodeError`
// suberror. The per-codec helpers (decode_bmp, decode_qoi, ...) and the
// low-level byte readers (read_u32_le, ...) keep raising the MoonBit
// built-in `Failure` suberror; the wrappers below translate any
// `Failure` they surface into a `DecodeError::InvalidHeader` (the
// underlying message is preserved so debug logs still tell you what the
// codec actually rejected).
//-----------------------------------------------------------------------------

///|
/// Auto-detect format and decode an image from raw bytes.
///
/// Raises `DecodeError::UnsupportedFormat` if the byte stream does not
/// match any known signature. Wraps every codec-internal `Failure` as
/// `DecodeError::InvalidHeader` so callers only need to match on one
/// error type.
pub fn decode(data : Bytes) -> Image raise DecodeError {
  match detect_format(data) {
    None => raise DecodeError::UnsupportedFormat("unknown image format")
    Some(format) =>
      match (try? decode_by_format_raw(data, format)) {
        Ok(img) => img
        Err(Failure::Failure(msg)) => raise DecodeError::InvalidHeader(msg)
      }
  }
}

///|
/// Decode an image with a known format. Internal codec failures are
/// wrapped as `DecodeError::InvalidHeader`; no format-detection step
/// happens, so `UnsupportedFormat` is not raised from this entry point.
pub fn decode_by_format(
  data : Bytes,
  format : ImageFormat,
) -> Image raise DecodeError {
  match (try? decode_by_format_raw(data, format)) {
    Ok(img) => img
    Err(Failure::Failure(msg)) => raise DecodeError::InvalidHeader(msg)
  }
}

///|
/// Internal: dispatch to the right per-format raw decoder and let its
/// own `Failure` propagate. The two public wrappers above catch that
/// failure and re-raise as `DecodeError`.
fn decode_by_format_raw(
  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)
    ImageFormat::ICO => decode_ico(data)
    ImageFormat::TIFF => decode_tiff(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 DecodeError {
  match detect_format(data) {
    None => raise DecodeError::UnsupportedFormat("unknown image format")
    Some(format) =>
      match (try? image_dimensions_raw(data, format)) {
        Ok(dims) => dims
        Err(Failure::Failure(msg)) => raise DecodeError::InvalidHeader(msg)
      }
  }
}

///|
/// Internal: dispatch to the right per-format raw header reader.
fn image_dimensions_raw(
  data : Bytes,
  format : ImageFormat,
) -> (Int, Int, ImageFormat) raise Failure {
  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)
    }
    // ICO + TIFF require walking the directory; we fall back to
    // decode() for them and let the byte-level pixel data carry the
    // dimensions. This keeps the fast path cheap for the simple
    // formats where it matters.
    ImageFormat::ICO | ImageFormat::TIFF => {
      let img = decode_by_format_raw(data, format)
      (img.width, img.height, format)
    }
  }
}

///|
/// 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 = read_u32_le(data, 18)
  // BMP uses a signed int32 for height: a negative value means
  // top-down row order; we only care about magnitude here.
  let height_raw = read_u32_le(data, 22)
  let height = if height_raw < 0 { -height_raw } else { height_raw }
  (width, 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
//-----------------------------------------------------------------------------

///|
/// 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"
    ImageFormat::ICO => "ICO"
    ImageFormat::TIFF => "TIFF"
  }
}

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

///|
/// Encode an image to the specified format. Currently BMP and QOI have
/// encoders; other formats raise `DecodeError::EncodeNotImplemented`.
pub fn encode(img : Image, format : ImageFormat) -> Bytes raise DecodeError {
  match format {
    ImageFormat::BMP =>
      match (try? encode_bmp(img)) {
        Ok(bytes) => bytes
        Err(Failure::Failure(msg)) => raise DecodeError::InvalidHeader(msg)
      }
    ImageFormat::QOI =>
      match (try? encode_qoi(img)) {
        Ok(bytes) => bytes
        Err(Failure::Failure(msg)) => raise DecodeError::InvalidHeader(msg)
      }
    _ =>
      raise DecodeError::EncodeNotImplemented(
        "encoding not yet supported for " + image_format_name(format),
      )
  }
}