///|
/// Core PDF object tree.
pub(all) enum PdfObject {
  Null
  Boolean(Bool)
  Integer(Int)
  Real(Double)
  String(String)
  Name(PdfName)
  Array(Array[PdfObject])
  // Use an Array of (key, value) pairs instead of 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)])
  Stream(Ref[(PdfObject, Stream)])
  Indirect(Int)
}

///|
/// 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 {
  to_pdf_number(Self) -> PdfObject
}

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

///|
pub impl ToPdfNumber for Double with to_pdf_number(self : Double) -> PdfObject {
  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::new(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::new(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::new(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")
  }
}