// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// Which format a run of bytes is, decided by what the format starts with.
//
// Every format worth the name puts a signature at the front, so the first four
// to twelve bytes are enough. The exception is TGA, which has none: it was
// designed when a file was told what it was by its extension. TGA is recognised
// by its end-of-file signature where one exists, and otherwise by a header that
// could not be anything else — a real TGA is the last resort, after every
// format with a signature has been ruled out.

///|
/// The formats this library reads and writes.
pub(all) enum Format {
  /// PNG, with APNG for animation.
  Png
  /// Quite OK Image.
  Qoi
  /// Windows bitmap.
  Bmp
  /// Truevision TGA.
  Tga
  /// JPEG, baseline and progressive.
  Jpeg
  /// GIF, with animation.
  Gif
  /// WebP, lossless, lossy and animated.
  Webp
} derive(Eq, Debug)

///|
pub extend Format with Eq::{equal, not_equal}

///|
pub extend Format with Debug::{to_repr}

///|
/// A format writes itself as its extension, which is what a person calls it.
pub impl Show for Format with fn output(self, logger) {
  logger.write_string(self.extension())
}

///|
pub extend Format with Show::{to_string, output}

///|
/// The extension a file of this format usually has, without the dot.
pub fn Format::extension(self : Format) -> String {
  match self {
    Png => "png"
    Qoi => "qoi"
    Bmp => "bmp"
    Tga => "tga"
    Jpeg => "jpg"
    Gif => "gif"
    Webp => "webp"
  }
}

///|
/// The media type a server would send for this format.
pub fn Format::mime(self : Format) -> String {
  match self {
    Png => "image/png"
    Qoi => "image/qoi"
    Bmp => "image/bmp"
    Tga => "image/x-tga"
    Jpeg => "image/jpeg"
    Gif => "image/gif"
    Webp => "image/webp"
  }
}

///|
/// Which format these bytes are, by signature.
///
/// Raises `Unknown` when nothing matches and `Truncated` when there are too few
/// bytes to tell: two bytes of a PNG are not a PNG, and saying so is better than
/// guessing.
pub fn Format::of(bytes : BytesView) -> Format raise Refused {
  let length = bytes.length()
  if length >= 8 && bytes.has_prefix(png_signature) {
    return Png
  }
  if length >= 3 && bytes.has_prefix(b"\xFF\xD8\xFF") {
    return Jpeg
  }
  if length >= 6 && (bytes.has_prefix(b"GIF87a") || bytes.has_prefix(b"GIF89a")) {
    return Gif
  }
  if length >= 12 &&
    bytes.has_prefix(b"RIFF") &&
    bytes[8:12].equal_to_bytes(b"WEBP") {
    return Webp
  }
  if length >= 4 && bytes.has_prefix(b"qoif") {
    return Qoi
  }
  if length >= 2 && bytes.has_prefix(b"BM") {
    return Bmp
  }
  if is_tga(bytes) {
    return Tga
  }
  if length < 8 {
    raise Truncated(at=0, want=8)
  }
  raise Unknown(at=0, got=hex(bytes, 0, 8))
}

///|
/// The eight bytes every PNG starts with, which are there to catch a file that
/// travelled through a text mode.
let png_signature : Bytes = b"\x89PNG\r\n\x1a\n"

///|
/// Whether these bytes are a TGA, which has no signature to say so.
///
/// Two tests, both cheap: the footer TGA 2.0 defines, and a header whose fields
/// only a TGA could have — an image type in the set of six, a pixel depth in the
/// set of four, and a colour map declaration that agrees with the image type.
fn is_tga(bytes : BytesView) -> Bool {
  let length = bytes.length()
  if length >= 26 &&
    bytes[length - 18:length - 16].equal_to_bytes(b"TRUEVISION-XFILE") {
    return true
  }
  if length < 18 {
    return false
  }
  let image_type = bytes[2].to_int()
  let depth = bytes[16].to_int()
  let width = bytes[12].to_int() | (bytes[13].to_int() << 8)
  let height = bytes[14].to_int() | (bytes[15].to_int() << 8)
  let mapped = bytes[1].to_int() <= 1
  let known_type = match image_type {
    1 | 2 | 3 | 9 | 10 | 11 => true
    _ => false
  }
  let known_depth = match depth {
    8 | 15 | 16 | 24 | 32 => true
    _ => false
  }
  let needs_map = match image_type {
    1 | 9 => true
    _ => false
  }
  known_type &&
  known_depth &&
  mapped &&
  (if needs_map { bytes[1].to_int() == 1 } else { true }) &&
  width > 0 &&
  height > 0 &&
  bytes[0].to_int() + 18 < length
}

///|
/// A few bytes as hexadecimal, for the `got` field of a refusal.
pub fn hex(bytes : BytesView, at : Int32, count : Int32) -> String {
  let mut out = ""
  let end = if at + count > bytes.length() {
    bytes.length()
  } else {
    at + count
  }
  for i in at.. at {
      out = out + " "
    }
    out = out + hex_byte(bytes[i].to_int())
  }
  out
}

///|
fn hex_byte(value : Int32) -> String {
  let digits = "0123456789abcdef"
  let hi = (value >> 4) & 0xF
  let lo = value & 0xF
  digits[hi:hi + 1].to_owned() + digits[lo:lo + 1].to_owned()
}