///|
/// A JSON-shaped semantic value. Requests and reports travel as these; object
/// key order is never significant, and numbers are finite doubles.
pub(all) enum Value {
  Null
  Bool(Bool)
  Number(Double)
  String(String)
  Array(Array[Value])
  Object(Map[String, Value])
} derive(Eq, @debug.Debug)

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

///|
pub extend Value with @debug.Debug::{to_repr}

///|
pub fn from_json(value : Json) -> Value {
  match value {
    Json::Null => Null
    Json::True => Bool(true)
    Json::False => Bool(false)
    Json::Number(n, ..) => Number(n)
    Json::String(s) => String(s)
    Json::Array(items) => Array(items.map(from_json))
    Json::Object(fields) => Object(fields.map(fn(_, v) { from_json(v) }))
  }
}

///|
pub fn Value::to_json(self : Value) -> Json {
  match self {
    Null => Json::null()
    Bool(b) => Json::boolean(b)
    Number(n) => Json::number(n)
    String(s) => Json::string(s)
    Array(items) => Json::array(items.map(fn(v) { v.to_json() }))
    Object(fields) => Json::object(fields.map(fn(_, v) { v.to_json() }))
  }
}

///|
/// Character-lexical key order, so a report hashed or compared across backends
/// and languages always spells the same.
pub fn Value::canonical(self : Value) -> String {
  match self {
    Object(fields) => {
      let keys = fields.keys().collect()
      keys.sort_by(fn(a, b) { lexical_compare(a, b) })
      "{" +
      keys
      .map(fn(k) {
        String(k).to_json().stringify() + ":" + fields[k].canonical()
      })
      .join(",") +
      "}"
    }
    Array(items) => "[" + items.map(fn(v) { v.canonical() }).join(",") + "]"
    _ => self.to_json().stringify()
  }
}

///|
fn lexical_compare(left : String, right : String) -> Int32 {
  let a = left.to_array()
  let b = right.to_array()
  let size = if a.length() < b.length() { a.length() } else { b.length() }
  for i in 0.. b[i] {
      return 1
    }
  }
  a.length() - b.length()
}

///|
pub(all) suberror InputError {
  InputError(String)
} derive(Eq, @debug.Debug)

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

///|
pub extend InputError with @debug.Debug::{to_repr}

///|
pub impl Show for InputError with fn output(self, logger) {
  let InputError(message) = self
  logger.write_string(message)
}

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

///|
pub fn get(value : Value, key : String) -> Value {
  match value {
    Object(fields) => fields.get(key).unwrap_or(Null)
    _ => Null
  }
}

///|
pub fn field(value : Value, key : String) -> Value {
  get(value, key)
}

///|
pub fn has(value : Value, key : String) -> Bool {
  match value {
    Object(fields) => fields.contains(key)
    _ => false
  }
}

///|
pub fn required(value : Value, key : String) -> Value raise InputError {
  if !has(value, key) {
    raise InputError("missing field: " + key)
  }
  field(value, key)
}

///|
pub fn str(value : Value) -> String raise InputError {
  match value {
    String(s) => s
    _ => raise InputError("expected string")
  }
}

///|
pub fn optional_str(
  value : Value,
  fallback : String,
) -> String raise InputError {
  match value {
    Null => fallback
    _ => str(value)
  }
}

///|
pub fn arr(value : Value) -> Array[Value] raise InputError {
  match value {
    Array(items) => items
    _ => raise InputError("expected array")
  }
}

///|
pub fn obj(value : Value) -> Map[String, Value] raise InputError {
  match value {
    Object(fields) => fields
    _ => raise InputError("expected object")
  }
}

///|
pub fn num(value : Value) -> Double raise InputError {
  match value {
    Number(n) if !n.is_nan() && !n.is_inf() => n
    _ => raise InputError("expected finite number")
  }
}

///|
pub fn optional_num(
  value : Value,
  fallback : Double,
) -> Double raise InputError {
  match value {
    Null => fallback
    _ => num(value)
  }
}

///|
pub fn record(fields : Array[(String, Value)]) -> Value {
  let result : Map[String, Value] = Map([])
  for (key, value) in fields {
    result[key] = value
  }
  Object(result)
}

///|
pub fn strings(items : Array[String]) -> Value {
  Array(items.map(fn(s) { String(s) }))
}

///|
pub fn fields_only(
  value : Value,
  allowed : Array[String],
) -> Unit raise InputError {
  for key, _ in obj(value) {
    if !allowed.contains(key) {
      raise InputError("unknown field: " + key)
    }
  }
}