///|
/// Core PDF object tree.
///
/// This is a low-level representation close to PDF syntax. In particular:
/// - `PdfObject::PdfString` is treated as a byte string by the parser/serializer.
/// - `PdfObject::Dictionary` keys are also stored as "bytes-in-String" so we can
///   round-trip weird/malformed PDFs without losing information.
pub(all) enum PdfObject {
  /// PDF `null`.
  Null
  /// PDF boolean (`true` or `false`).
  Boolean(Bool)
  /// PDF integer number.
  Integer(Int)
  /// PDF real number.
  Real(Double)
  /// PDF string object (either literal `(...)` or hexadecimal `<...>`).
  ///
  /// Note: this is stored in a MoonBit `String` but treated as raw bytes by the
  /// parser/serializer (each UTF-16 code unit stores one byte).
  PdfString(String)
  /// PDF name object (written like `/Type`).
  ///
  /// Names are byte sequences, not Unicode text; see `PdfName`.
  Name(PdfName)
  /// PDF array object (ordered list).
  PdfArray(Array[PdfObject])
  /// PDF dictionary object.
  ///
  /// Stored as an array of `(key, value)` pairs instead of a map:
  /// - Allows representing malformed PDFs with duplicate keys.
  /// - Preserves "first match wins" semantics used by `lookup_*` (search_by).
  /// - Keeps insertion order stable for serialization.
  /// - Key encoding: `String` is treated as a "bytes-in-String" byte string,
  ///   matching the parser/serializer (each UTF-16 code unit stores one byte).
  Dictionary(Array[(String, PdfObject)])
  /// PDF stream object.
  ///
  /// Streams are represented as a `(stream_dict, stream_data)` pair stored in a
  /// mutable reference so code can "materialize" deferred bytes in-place.
  Stream(Ref[(PdfObject, Stream)])
  /// PDF indirect reference (`n 0 R`) to an object number.
  Indirect(Int)
} derive(Debug)

///|
/// Convert primitive numeric types into PDF numeric objects.
///
/// This is intentionally narrow (only Int/Double) so callers don't have to
/// choose between PDF Strings vs Names, etc.
pub trait ToPdfNumber {
  fn to_pdf_number(Self) -> PdfObject
}

///|
pub impl ToPdfNumber for Int with fn to_pdf_number(self : Int) -> PdfObject {
  Integer(self)
}

///|
pub impl ToPdfNumber for Double with fn to_pdf_number(self : Double) -> PdfObject {
  Real(self)
}

///|
/// Remove a dictionary entry (also works for streams).
pub fn PdfObject::remove_entry(
  self : PdfObject,
  key : String,
) -> PdfObject raise {
  match self {
    Dictionary(entries) => {
      let out = entries.filter(pair => pair.0 != key)
      Dictionary(out)
    }
    Stream(r) => {
      let (inner, stream) = r.val
      r.val = (inner.remove_entry(key), stream)
      Stream(r)
    }
    _ => raise PdfError::Msg("remove_entry: not a dictionary")
  }
}

///|
/// Replace a dictionary entry, raising if it's not present (also works for streams).
pub fn PdfObject::replace_entry(
  self : PdfObject,
  key : String,
  value : PdfObject,
) -> PdfObject raise {
  match self {
    Null => Dictionary([(key, value)])
    Dictionary(entries) => {
      let mut replaced = false
      let out = Array(capacity=entries.length())
      for entry in entries {
        let (k, v) = entry
        if k == key {
          out.push((k, value))
          replaced = true
        } else {
          out.push((k, v))
        }
      }
      if !replaced {
        raise PdfError::Msg("replace_entry: key not found")
      }
      Dictionary(out)
    }
    Stream(r) => {
      let (inner, stream) = r.val
      r.val = (inner.replace_entry(key, value), stream)
      Stream(r)
    }
    _ => raise PdfError::Msg("replace_entry: not a dictionary")
  }
}

///|
/// Add a dictionary entry, replacing if already present (also works for streams).
pub fn PdfObject::add_entry(
  self : PdfObject,
  key : String,
  value : PdfObject,
) -> PdfObject raise {
  match self {
    Null => Dictionary([(key, value)])
    Dictionary(entries) => {
      let mut replaced = false
      let out = Array(capacity=entries.length() + 1)
      for entry in entries {
        let (k, v) = entry
        if k == key {
          out.push((k, value))
          replaced = true
        } else {
          out.push((k, v))
        }
      }
      if !replaced {
        out.push((key, value))
      }
      Dictionary(out)
    }
    Stream(r) => {
      let (inner, stream) = r.val
      r.val = (inner.add_entry(key, value), stream)
      Stream(r)
    }
    _ => raise PdfError::Msg("add_entry: not a dictionary")
  }
}

///|
/// Lookup the key without following indirects at either source or destination.
pub fn PdfObject::lookup_immediate(
  self : PdfObject,
  key : String,
) -> PdfObject? {
  fn lookup_entries(entries : Array[(String, PdfObject)]) -> PdfObject? {
    match entries.search_by(pair => pair.0 == key) {
      None => None
      Some(i) => Some(entries[i].1)
    }
  }

  match self {
    Dictionary(entries) => lookup_entries(entries)
    Stream(r) => {
      let (inner, _) = r.val
      match inner {
        Dictionary(entries) => lookup_entries(entries)
        _ => None
      }
    }
    _ => None
  }
}

///|
/// Lookup all values for a key without following indirects.
///
/// This is useful for handling malformed PDFs where dictionaries may contain
/// duplicate keys.
pub fn PdfObject::lookup_immediate_all(
  self : PdfObject,
  key : String,
) -> Array[PdfObject] {
  fn collect(entries : Array[(String, PdfObject)]) -> Array[PdfObject] {
    let out = Array(capacity=entries.length())
    for entry in entries {
      let (k, v) = entry
      if k == key {
        out.push(v)
      }
    }
    out
  }

  match self {
    Dictionary(entries) => collect(entries)
    Stream(r) => {
      let (inner, _) = r.val
      match inner {
        Dictionary(entries) => collect(entries)
        _ => []
      }
    }
    _ => []
  }
}

///|
/// Find the indirect reference given by the value associated with a key.
pub fn PdfObject::find_indirect(self : PdfObject, key : String) -> Int? raise {
  match self {
    Dictionary(_) =>
      match self.lookup_immediate(key) {
        Some(Indirect(i)) => Some(i)
        _ => None
      }
    _ => raise PdfError::Msg("find_indirect: not a dictionary")
  }
}

// Explicit method promotion for derived/implemented traits.
// (Previously implicit; see warning [0079] implicit_impl_as_method.)

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