///|
/// The text is not JSON.
pub let parse_error : Int32 = -32700

///|
/// The value is not a valid message.
pub let invalid_request : Int32 = -32600

///|
/// Nobody registered that method.
pub let method_not_found : Int32 = -32601

///|
/// The parameters are not what the method takes.
pub let invalid_params : Int32 = -32602

///|
/// The handler failed in a way the caller is not told about.
pub let internal_error : Int32 = -32603

///|
/// A request's identifier. A number keeps the text it was written in, as core's `Json` does, so an
/// identifier `Double` cannot hold goes back out exactly as it came in.
#derive(Eq, Debug)
pub(all) enum Id {
  Num(Double, repr~ : String?)
  Text(String)
  Null
}

///|
/// An identifier from an integer.
pub fn Id::int(n : Int64) -> Id {
  Num(n.to_double(), repr=None)
}

///|
/// The same text for the same identifier, whichever side wrote it: a map key for matching replies
/// to calls.
pub fn Id::key(self : Id) -> String {
  match self {
    Num(_, repr=Some(repr)) => "n:\{repr}"
    Num(number, repr=None) => "n:\{number}"
    Text(text) => "s:\{text}"
    Null => "null"
  }
}

///|
pub impl ToJson for Id with fn to_json(self) {
  match self {
    Num(number, repr~) => Json::number(number, repr?)
    Text(text) => Json::string(text)
    Null => Json::null()
  }
}

///|
/// The error object of a response.
#derive(Eq, Debug)
pub(all) struct Failure {
  code : Int32
  message : String
  data : Json?
}

///|
pub impl ToJson for Failure with fn to_json(self) {
  let out : Map[String, Json] = {
    "code": Json::number(self.code.to_double()),
    "message": Json::string(self.message),
  }
  if self.data is Some(data) {
    out["data"] = data
  }
  Json(out)
}

///|
/// What goes wrong with a call. `Failed` is the other side's error object, and a handler raises it to
/// answer with one. `limit~` is the bound that was passed and `got~` what came.
#derive(Eq, Debug)
pub(all) suberror Failed {
  Failed(Failure)
  TimedOut(name~ : String, limit~ : Int32)
  Closed(name~ : String)
  TooLong(limit~ : Int32, got~ : Int64)
  Malformed(String)
}

///|
/// Raise an error object: `fail(invalid_params, "no such file")`.
pub fn[T] fail(code : Int32, message : String, data? : Json) -> T raise Failed {
  raise Failed({ code, message, data, })
}

///|
/// One JSON-RPC 2.0 message. A `Refusal` is a response carrying an error.
#derive(Eq, Debug)
pub(all) enum Message {
  Call(id~ : Id, name~ : String, params~ : Json?)
  Notice(name~ : String, params~ : Json?)
  Reply(id~ : Id, result~ : Json)
  Refusal(id~ : Id, failure~ : Failure)
}

///|
pub impl ToJson for Message with fn to_json(self) {
  let out : Map[String, Json] = { "jsonrpc": Json::string("2.0") }
  match self {
    Call(id~, name~, params~) => {
      out["id"] = id.to_json()
      out["method"] = Json::string(name)
      if params is Some(params) {
        out["params"] = params
      }
    }
    Notice(name~, params~) => {
      out["method"] = Json::string(name)
      if params is Some(params) {
        out["params"] = params
      }
    }
    Reply(id~, result~) => {
      out["id"] = id.to_json()
      out["result"] = result
    }
    Refusal(id~, failure~) => {
      out["id"] = id.to_json()
      out["error"] = failure.to_json()
    }
  }
  Json(out)
}

///|
fn id_of(value : Json) -> Id? {
  match value {
    Number(number, repr~) => Some(Num(number, repr~))
    String(text) => Some(Text(text))
    Null => Some(Null)
    _ => None
  }
}

///|
fn failure_of(value : Json) -> Failure? {
  guard value is Object(fields) &&
    fields.get("code") is Some(Number(code, ..)) &&
    fields.get("message") is Some(String(message)) &&
    code == code.to_int().to_double() else {
    return None
  }
  Some({ code: code.to_int(), message, data: fields.get("data"), })
}

///|
fn call_of(fields : Map[String, Json], name : Json) -> Message? {
  guard name is String(name) &&
    !fields.contains("result") &&
    !fields.contains("error") else {
    return None
  }
  let params = fields.get("params")
  match params {
    None | Some(Array(_)) | Some(Object(_)) => ()
    _ => return None
  }
  match fields.get("id") {
    None => Some(Notice(name~, params~))
    Some(id) => id_of(id).map(id => Call(id~, name~, params~))
  }
}

///|
fn response_of(fields : Map[String, Json]) -> Message? {
  guard fields.get("id").bind(id_of) is Some(id) else { return None }
  match (fields.get("result"), fields.get("error")) {
    (Some(result), None) =>
      if id == Null {
        None
      } else {
        Some(Reply(id~, result~))
      }
    (None, Some(error)) =>
      failure_of(error).map(failure => Refusal(id~, failure~))
    _ => None
  }
}

///|
fn message_of(json : Json) -> Message? {
  guard json is Object(fields) && fields.get("jsonrpc") is Some(String("2.0")) else {
    return None
  }
  match fields.get("method") {
    Some(name) => call_of(fields, name)
    None => response_of(fields)
  }
}

///|
/// Decodes by the specification's section 4 and 5: `jsonrpc` is exactly `"2.0"`, `params` is an array
/// or object when present, and a response has `result` or `error` but not both.
pub impl @json.FromJson for Message with fn from_json(json, path) {
  guard message_of(json) is Some(message) else {
    raise @json.JsonDecodeError((path, "not a JSON-RPC 2.0 message"))
  }
  message
}

///|
/// The answer to something that is not a valid message: -32600, with its identifier if one can be
/// read and null otherwise.
pub fn Message::invalid(json : Json) -> Message {
  let id = if json is Object(fields) {
    fields.get("id").bind(id_of).unwrap_or(Null)
  } else {
    Null
  }
  Refusal(id~, failure={
    code: invalid_request,
    message: "Invalid Request",
    data: None,
  })
}

///|
/// The answer to text that is not JSON: -32700, with a null identifier.
pub fn Message::unreadable() -> Message {
  Refusal(id=Null, failure={
    code: parse_error,
    message: "Parse error",
    data: None,
  })
}

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

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

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

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

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

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

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

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

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

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

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

///|
pub extend Message with @json.FromJson::{from_json}