///|
/// A value in a template. The context arrives as data; functions and host
/// objects enter as `Object`. `Markup` is text already safe to write as it
/// stands. `Undefined` is a name that resolved to nothing, carrying the name
/// for the message a later use may need.
pub(all) enum Value {
  Null
  Bool(Bool)
  Int(Int64)
  Double(Double)
  Text(String)
  Markup(String)
  Seq(Array[Value])
  Tuple(Array[Value])
  Dict(Map[String, Value])
  Undefined(String)
  Object(&Object)
}

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

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

///|
pub extend Value with ToJson::{to_json}

///|
/// Data read from JSON. A number written without a fraction or exponent is
/// an `Int` when it fits, as Python reads it.
pub fn Value::of_json(json : Json) -> Value {
  match json {
    Null => Null
    True => Bool(true)
    False => Bool(false)
    Number(d, repr~) => {
      let whole = match repr {
        Some(written) =>
          !written.contains(".") &&
          !written.contains("e") &&
          !written.contains("E")
        None => d == d.floor()
      }
      if whole && d.abs() < 9.2e18 {
        Int(d.to_int64())
      } else {
        Double(d)
      }
    }
    String(s) => Text(s)
    Array(items) => Seq(items.map(Value::of_json))
    Object(fields) => {
      let out = Map([])
      for name, item in fields {
        out[name] = Value::of_json(item)
      }
      Dict(out)
    }
  }
}

///|
/// Objects cross as the text they print: nothing on the other side could
/// call them.
pub impl ToJson for Value with fn to_json(self) {
  match self {
    Null | Undefined(_) => Json::null()
    Bool(b) => Json::boolean(b)
    Int(n) => Json::number(n.to_double(), repr=n.to_string())
    Double(d) => Json::number(d)
    Text(s) | Markup(s) => Json::string(s)
    Seq(items) | Tuple(items) => Json::array(items.map(item => Json(item)))
    Dict(fields) => {
      let out = Map([])
      for name, field in fields {
        out[name] = Json(field)
      }
      Json::object(out)
    }
    Object(_) => Json::string(self.to_string())
  }
}

///|
/// The way `{{ value }}` prints before escaping: Python's `str`.
pub impl Show for Value with fn output(self, logger) {
  match self {
    Text(s) | Markup(s) => logger.write_string(s)
    Undefined(_) => ()
    Object(obj) => logger.write_string(obj.show())
    _ => logger.write_string(self.repr())
  }
}

///|
/// Python's `repr`: text quoted, lists and mappings with their elements in
/// `repr` form. `pprint` and the printing of containers use it.
pub fn Value::repr(self : Value) -> String {
  match self {
    Null => "None"
    Bool(b) => if b { "True" } else { "False" }
    Int(n) => n.to_string()
    Double(d) => python_float(d)
    Text(s) => repr_text(s)
    Markup(s) => "Markup(\{repr_text(s)})"
    Seq(items) => "[" + items.map(Value::repr).join(", ") + "]"
    Tuple([one]) => "(" + one.repr() + ",)"
    Tuple(items) => "(" + items.map(Value::repr).join(", ") + ")"
    Dict(fields) => {
      let parts = []
      for name, field in fields {
        parts.push(repr_text(name) + ": " + field.repr())
      }
      "{" + parts.join(", ") + "}"
    }
    Undefined(_) => "Undefined"
    Object(obj) => obj.show()
  }
}

///|
/// What a condition sees: empty, zero, `None` and undefined are false.
pub fn Value::truthy(self : Value) -> Bool {
  match self {
    Null | Undefined(_) => false
    Bool(b) => b
    Int(n) => n != 0
    Double(d) => d != 0.0
    Text(s) | Markup(s) => !s.is_empty()
    Seq(items) | Tuple(items) => !items.is_empty()
    Dict(fields) => !fields.is_empty()
    Object(_) => true
  }
}

///|
/// Python's equality: numbers across `Int`, `Double` and `Bool`, text across
/// plain and safe, containers element by element, objects by identity.
pub impl Eq for Value with fn equal(self, other) {
  match (self, other) {
    (Null, Null) | (Undefined(_), Undefined(_)) => true
    (Text(a) | Markup(a), Text(b) | Markup(b)) => a == b
    (Seq(a), Seq(b)) | (Tuple(a), Tuple(b)) => a == b
    (Dict(a), Dict(b)) =>
      a.length() == b.length() &&
      a.iter().all(pair => b.get(pair.0) is Some(v) && v == pair.1)
    (Object(a), Object(b)) => physical_equal(a, b)
    _ =>
      match (self.number(), other.number()) {
        (Some(a), Some(b)) => a == b
        _ => false
      }
  }
}

///|
/// A number as Python sees one: `Bool` counts, being an `int`.
pub fn Value::number(self : Value) -> Double? {
  match self {
    Bool(b) => Some(if b { 1.0 } else { 0.0 })
    Int(n) => Some(n.to_double())
    Double(d) => Some(d)
    _ => None
  }
}

///|
/// Python's ordering: numbers with numbers, text with text, sequences
/// element by element. Anything else has no order and is refused.
pub fn Value::compare(self : Value, other : Value) -> Int32 raise Refused {
  match (self, other) {
    (Int(a), Int(b)) => a.compare(b)
    (Text(a) | Markup(a), Text(b) | Markup(b)) => code_point_compare(a, b)
    (Seq(a), Seq(b)) | (Tuple(a), Tuple(b)) => {
      for i in 0..
      match (self.number(), other.number()) {
        (Some(a), Some(b)) => a.compare(b)
        _ =>
          raise invalid("\{self.kind()} and \{other.kind()} cannot be compared")
      }
  }
}

///|
/// Strings ordered by code point, as Python orders them.
pub fn code_point_compare(a : String, b : String) -> Int32 {
  let x = a.iter()
  let y = b.iter()
  for ;; {
    match (x.next(), y.next()) {
      (Some(p), Some(q)) => if p != q { return p.compare(q) }
      (None, None) => return 0
      (None, _) => return -1
      (_, None) => return 1
    }
  }
}

///|
/// The word a message uses for a value's type.
pub fn Value::kind(self : Value) -> String {
  match self {
    Null => "none"
    Bool(_) => "a boolean"
    Int(_) => "an integer"
    Double(_) => "a float"
    Text(_) => "a string"
    Markup(_) => "markup"
    Seq(_) => "a list"
    Tuple(_) => "a tuple"
    Dict(_) => "a mapping"
    Undefined("") => "an undefined value"
    Undefined(name) => "\{name}, which is undefined,"
    Object(obj) => obj.kind()
  }
}

///|
/// The items a loop walks: a list's elements, a mapping's keys in insertion
/// order, a string's characters, or what an object yields.
pub fn Value::iter(self : Value) -> Array[Value] raise Refused {
  match self {
    Seq(items) | Tuple(items) => items.copy()
    Dict(fields) => fields.keys().map(name => Value::Text(name)).collect()
    Text(s) | Markup(s) =>
      s.iter().map(ch => Value::Text(ch.to_string())).collect()
    Object(obj) =>
      match obj.items() {
        Some(items) => items
        None => raise invalid("\{obj.kind()} cannot be iterated")
      }
    _ => raise invalid("\{self.kind()} cannot be iterated")
  }
}

///|
/// `value[key]`: a mapping by key, a list or string by index (negative
/// counts from the end), an object by attribute. `None` when there is
/// nothing there.
pub fn Value::item(self : Value, key : Value) -> Value? {
  match (self, key) {
    (Dict(fields), _) => fields.get(key.to_string())
    (Seq(items) | Tuple(items), _) =>
      match key.index() {
        Some(i) => {
          let at = if i < 0 { i + items.length() } else { i }
          if at >= 0 && at < items.length() {
            Some(items[at])
          } else {
            None
          }
        }
        None => None
      }
    (Text(s) | Markup(s), _) =>
      match key.index() {
        Some(i) => {
          let chars = s.iter().collect()
          let at = if i < 0 { i + chars.length() } else { i }
          if at >= 0 && at < chars.length() {
            Some(Text(chars[at].to_string()))
          } else {
            None
          }
        }
        None => None
      }
    (Object(obj), Text(name)) => obj.attr(name)
    (Object(obj), _) =>
      match (obj.items(), key.index()) {
        (Some(items), Some(i)) => Tuple(items).item(Int(i.to_int64()))
        _ => None
      }
    _ => None
  }
}

///|
/// `value.name`: an object's attribute, else the item of that name.
pub fn Value::field(self : Value, name : String) -> Value? {
  match self {
    Object(obj) => obj.attr(name)
    _ => self.item(Text(name))
  }
}

///|
/// A dotted path such as `user.address.0`, each step a lookup by item; a
/// step written in digits indexes a list.
pub fn Value::path(self : Value, path : String) -> Value? {
  let mut at = self
  for part in path.split(".") {
    let key : Value = Int(@string.parse_int64(part)) catch {
      _ => Text(part.to_owned())
    }
    match at.item(key) {
      Some(next) => at = next
      None => return None
    }
  }
  Some(at)
}

///|
/// A whole number usable as an index.
pub fn Value::index(self : Value) -> Int32? {
  match self {
    Int(n) if n >= -2147483648 && n <= 2147483647 => Some(n.to_int())
    Bool(b) => Some(if b { 1 } else { 0 })
    _ => None
  }
}

///|
/// A float the way Python writes it: the shortest digits that read back the
/// same, fixed between `1e-4` and `1e16`, scientific outside.
pub fn python_float(d : Double) -> String {
  if d.is_nan() {
    return "nan"
  }
  if d.is_inf() {
    return if d > 0.0 { "inf" } else { "-inf" }
  }
  let sign = if d < 0.0 || (d == 0.0 && 1.0 / d < 0.0) { "-" } else { "" }
  let (digits, exponent) = shortest_digits(d.abs())
  let body = if exponent >= -4 && exponent < 16 {
    if exponent >= 0 {
      let whole = exponent + 1
      if digits.length() <= whole {
        digits + "0".repeat(whole - digits.length()) + ".0"
      } else {
        digits[:whole].to_owned() + "." + digits[whole:].to_owned()
      }
    } else {
      "0." + "0".repeat(-exponent - 1) + digits
    }
  } else {
    let mantissa = if digits.length() == 1 {
      digits
    } else {
      digits[:1].to_owned() + "." + digits[1:].to_owned()
    }
    let sign_e = if exponent < 0 { "-" } else { "+" }
    let e = exponent.abs()
    mantissa + "e" + sign_e + (if e < 10 { "0" } else { "" }) + e.to_string()
  }
  sign + body
}

///|
/// The shortest decimal digits of a non-negative finite double and the
/// exponent of the first digit, read off `Double::to_string`, which already
/// prints the shortest round trip.
fn shortest_digits(d : Double) -> (String, Int32) {
  if d == 0.0 {
    return ("0", 0)
  }
  let text = d.to_string()
  let (mantissa, shift) = match text.find("e") {
    Some(at) => {
      let e = @string.parse_int(text[at + 1:]) catch { _ => 0 }
      (text[:at].to_owned(), e)
    }
    None => (text, 0)
  }
  let (whole, fraction) = match mantissa.find(".") {
    Some(at) => (mantissa[:at].to_owned(), mantissa[at + 1:].to_owned())
    None => (mantissa, "")
  }
  let all = whole + fraction
  let mut lead = 0
  while lead < all.length() - 1 && all[lead] == '0' {
    lead += 1
  }
  let mut tail = all.length()
  while tail > lead + 1 && all[tail - 1] == '0' {
    tail -= 1
  }
  let digits = all[lead:tail].to_owned()
  (digits, whole.length() - 1 - lead + shift)
}

///|
/// Python's `repr` of a string: single quotes unless the text holds one and
/// no double quote, with the escapes Python writes.
pub fn repr_text(s : String) -> String {
  let quote = if s.contains("'") && !s.contains("\"") { '"' } else { '\'' }
  let out = StringBuilder()
  out.write_char(quote)
  for ch in s {
    match ch {
      '\\' => out.write_string("\\\\")
      '\n' => out.write_string("\\n")
      '\r' => out.write_string("\\r")
      '\t' => out.write_string("\\t")
      c if c == quote => {
        out.write_char('\\')
        out.write_char(c)
      }
      c if c.to_int() < 0x20 || c.to_int() == 0x7F => {
        let hex = c.to_int().to_string(radix=16)
        out.write_string(
          "\\x" + (if hex.length() < 2 { "0" } else { "" }) + hex,
        )
      }
      c => out.write_char(c)
    }
  }
  out.write_char(quote)
  out.to_string()
}

///|
/// Python's `item in self`: a substring of a string, an element of a list
/// or tuple, a key of a mapping, what an object yields.
pub fn Value::contains(self : Value, item : Value) -> Bool raise Refused {
  match self {
    Text(s) | Markup(s) =>
      match item {
        Text(t) | Markup(t) => s.contains(t)
        _ =>
          raise invalid(
            "'in ' requires a string as left operand, not \{item.kind()}",
          )
      }
    Seq(items) | Tuple(items) => items.contains(item)
    Dict(fields) =>
      match item {
        Text(k) | Markup(k) => fields.contains(k)
        Int(_) | Double(_) | Bool(_) | Null => fields.contains(item.to_string())
        _ => false
      }
    Object(obj) =>
      match obj.items() {
        Some(items) => items.contains(item)
        None => raise invalid("\{obj.kind()} cannot be searched with in")
      }
    _ => raise invalid("\{self.kind()} cannot be searched with in")
  }
}