///|
/// An attribute value. Ruby Asciidoctor stores strings, integers, floats,
/// booleans and nil in attribute hashes; this enum mirrors that.
pub(all) enum AttrVal {
  Str(String)
  Int(Int)
  Float(Double)
  Bool(Bool)
  List(Array[String])
  Nil
} derive(Eq, Debug)

///|
/// Ruby `to_s` of the value (`nil` → "").
pub fn AttrVal::to_s(self : AttrVal) -> String {
  match self {
    Str(s) => s
    Int(i) => i.to_string()
    Float(f) => @rb.float_to_s(f)
    Bool(b) => if b { "true" } else { "false" }
    List(l) => "[" + l.map(x => x.escape()).join(", ") + "]"
    Nil => ""
  }
}

///|
/// Ruby truthiness: only nil and false are falsy.
pub fn AttrVal::truthy(self : AttrVal) -> Bool {
  match self {
    Nil | Bool(false) => false
    _ => true
  }
}

///|
/// Ruby `to_i` of the value.
pub fn AttrVal::to_i(self : AttrVal) -> Int {
  match self {
    Str(s) => @rb.to_i(s)
    Int(i) => i
    Float(f) => f.to_int()
    _ => 0
  }
}

///|
/// Ruby `to_f` of the value.
pub fn AttrVal::to_f(self : AttrVal) -> Double {
  match self {
    Str(s) => @rb.to_f(s)
    Int(i) => i.to_double()
    Float(f) => f
    _ => 0.0
  }
}

///|
/// Ruby `==` against a string.
pub fn AttrVal::is_str(self : AttrVal, s : String) -> Bool {
  self is Str(v) && v == s
}

///|
/// Key of an attribute: named (String) or positional (1-based Int).
pub(all) enum AttrKey {
  Name(String)
  Pos(Int)
} derive(Eq, Hash, Debug)

///|
/// A document attribute assignment recorded in the body of a document and
/// replayed during conversion (Ruby `Document::AttributeEntry`).
pub(all) struct AttributeEntry {
  name : String
  value : String?
  negate : Bool
} derive(Debug)

///|
pub fn AttributeEntry::new(
  name : String,
  value : String?,
  negate? : Bool,
) -> AttributeEntry {
  { name, value, negate: negate.unwrap_or(value is None), }
}

///|
/// An insertion-ordered attribute map (Ruby Hash semantics) plus the
/// attribute entries that Ruby stores under the `:attribute_entries` key.
pub struct Attributes {
  priv map : Map[AttrKey, AttrVal]
  mut entries : Array[AttributeEntry]?
}

///|
pub fn Attributes::new() -> Attributes {
  { map: {}, entries: None, }
}

///|
pub fn Attributes::from_array(pairs : Array[(String, AttrVal)]) -> Attributes {
  let a = Attributes::new()
  for p in pairs {
    a.map[Name(p.0)] = p.1
  }
  a
}

///|
/// Shallow copy (Ruby `Hash#merge` with no arguments).
pub fn Attributes::copy(self : Attributes) -> Attributes {
  let m : Map[AttrKey, AttrVal] = Map([])
  for k, v in self.map {
    m[k] = v
  }
  { map: m, entries: self.entries.map(e => e.copy()), }
}

///|
pub fn Attributes::length(self : Attributes) -> Int {
  self.map.length()
}

///|
/// Whether the map is empty (the attribute entries count as a key, as in Ruby).
pub fn Attributes::is_empty(self : Attributes) -> Bool {
  self.map.is_empty() && self.entries is None
}

///|
pub fn Attributes::clear(self : Attributes) -> Unit {
  self.map.clear()
  self.entries = None
}

///|
/// Replaces all contents with those of `other` (Ruby `Hash#replace`).
pub fn Attributes::replace(self : Attributes, other : Attributes) -> Unit {
  if physical_equal(self, other) {
    return
  }
  self.map.clear()
  for k, v in other.map {
    self.map[k] = v
  }
  self.entries = other.entries.map(e => e.copy())
}

///|
/// Merges `other` into self, overwriting (Ruby `Hash#update`).
pub fn Attributes::update(self : Attributes, other : Attributes) -> Unit {
  for k, v in other.map {
    self.map[k] = v
  }
  if other.entries is Some(e) {
    self.entries = Some(e.copy())
  }
}

///|
/// Raw value of a named attribute.
pub fn Attributes::get(self : Attributes, name : String) -> AttrVal? {
  self.map.get(Name(name))
}

///|
/// Raw value of a positional attribute.
pub fn Attributes::get_pos(self : Attributes, i : Int) -> AttrVal? {
  self.map.get(Pos(i))
}

///|
pub fn Attributes::get_key(self : Attributes, k : AttrKey) -> AttrVal? {
  self.map.get(k)
}

///|
/// Ruby `attrs[name]` coerced to a string when truthy (nil/false → None).
pub fn Attributes::str(self : Attributes, name : String) -> String? {
  match self.map.get(Name(name)) {
    Some(Nil) | Some(Bool(false)) | None => None
    Some(v) => Some(v.to_s())
  }
}

///|
/// Positional attribute as string when truthy.
pub fn Attributes::pos_str(self : Attributes, i : Int) -> String? {
  match self.map.get(Pos(i)) {
    Some(Nil) | Some(Bool(false)) | None => None
    Some(v) => Some(v.to_s())
  }
}

///|
/// Whether a named attribute has a truthy value (Ruby `if attrs[name]`).
pub fn Attributes::truthy(self : Attributes, name : String) -> Bool {
  match self.map.get(Name(name)) {
    Some(v) => v.truthy()
    None => false
  }
}

///|
/// Ruby `attrs.key?(name)`.
pub fn Attributes::contains(self : Attributes, name : String) -> Bool {
  self.map.contains(Name(name))
}

///|
pub fn Attributes::contains_pos(self : Attributes, i : Int) -> Bool {
  self.map.contains(Pos(i))
}

///|
pub fn Attributes::set(self : Attributes, name : String, v : AttrVal) -> Unit {
  self.map[Name(name)] = v
}

///|
pub fn Attributes::set_str(
  self : Attributes,
  name : String,
  v : String,
) -> Unit {
  self.map[Name(name)] = Str(v)
}

///|
pub fn Attributes::set_pos(self : Attributes, i : Int, v : AttrVal) -> Unit {
  self.map[Pos(i)] = v
}

///|
pub fn Attributes::set_key(self : Attributes, k : AttrKey, v : AttrVal) -> Unit {
  self.map[k] = v
}

///|
/// Ruby `attrs[name] ||= value`.
pub fn Attributes::set_default(
  self : Attributes,
  name : String,
  v : AttrVal,
) -> Unit {
  if !self.truthy(name) {
    self.map[Name(name)] = v
  }
}

///|
/// Removes a named attribute, returning its value.
pub fn Attributes::remove(self : Attributes, name : String) -> AttrVal? {
  let k = Name(name)
  let v = self.map.get(k)
  if v is Some(_) {
    self.map.remove(k)
  }
  v
}

///|
/// Removes a named attribute, returning its value as a string when truthy.
pub fn Attributes::remove_str(self : Attributes, name : String) -> String? {
  match self.remove(name) {
    Some(Nil) | Some(Bool(false)) | None => None
    Some(v) => Some(v.to_s())
  }
}

///|
pub fn Attributes::remove_pos(self : Attributes, i : Int) -> AttrVal? {
  let k = Pos(i)
  let v = self.map.get(k)
  if v is Some(_) {
    self.map.remove(k)
  }
  v
}

///|
pub fn Attributes::remove_key(self : Attributes, k : AttrKey) -> Unit {
  self.map.remove(k)
}

///|
/// Iterates over (key, value) pairs in insertion order.
pub fn Attributes::iter(self : Attributes) -> Iter2[AttrKey, AttrVal] {
  self.map.iter2()
}

///|
/// Named keys in insertion order.
pub fn Attributes::names(self : Attributes) -> Array[String] {
  let out = []
  for k, _ in self.map {
    if k is Name(n) {
      out.push(n)
    }
  }
  out
}

///|
/// Records an attribute entry (Ruby `AttributeEntry#save_to`).
pub fn Attributes::save_entry(
  self : Attributes,
  entry : AttributeEntry,
) -> Unit {
  match self.entries {
    Some(e) => e.push(entry)
    None => self.entries = Some([entry])
  }
}

///|
/// Ruby `attrs.delete :attribute_entries`.
pub fn Attributes::clear_entries(self : Attributes) -> Unit {
  self.entries = None
}