///|
fn json_number_value(value : Double, representation : String?) -> Value {
  match representation {
    Some(text) => {
      let is_integral = !text.contains(".") &&
        !text.contains("e") &&
        !text.contains("E")
      if is_integral {
        let parsed = @string.parse_int64(text) catch {
          _ => return Value::Double(value)
        }
        return Value::Int(parsed)
      }
      Value::Double(value)
    }
    None => Value::Double(value)
  }
}

///|
/// Convert JSON-expressible data to CEL values.
pub fn value_from_json(json : Json) -> Value raise JsonBridgeError {
  match json {
    Json::Null => Value::Null
    Json::True => Value::Bool(true)
    Json::False => Value::Bool(false)
    Json::Number(value, repr~) => json_number_value(value, repr)
    Json::String(value) => Value::String(value)
    Json::Array(items) => Value::List(items.map(item => value_from_json(item)))
    Json::Object(fields) => {
      let names = [ for name, _ in fields => name ]
      names.sort()
      let entries : Array[ValueEntry] = []
      for name in names {
        let value = fields.get(name).unwrap()
        entries.push({ key: Value::String(name), value: value_from_json(value) })
      }
      Value::Map(entries)
    }
  }
}

///|
fn tagged_value(kind : String, value : Json) -> Json {
  Json::object({ "@type": Json::string(kind), "value": value })
}

///|
/// Convert a CEL value to stable JSON. UInt and Bytes use tagged objects.
pub fn Value::to_json(self : Value) -> Json raise JsonBridgeError {
  match self {
    Value::Null => Json::null()
    Value::Bool(value) => Json::boolean(value)
    Value::Int(value) => Json::number(value.to_double(), repr=value.to_string())
    Value::UInt(value) => tagged_value("uint", Json::string(value.to_string()))
    Value::Double(value) => Json::number(value)
    Value::String(value) => Json::string(value)
    Value::Bytes(value) =>
      tagged_value(
        "bytes",
        Json::array(
          value.to_array().map(byte => Json::number(byte.to_int().to_double())),
        ),
      )
    Value::List(values) => Json::array(values.map(value => value.to_json()))
    Value::Map(entries) => {
      let object : Map[String, Json] = Map([])
      for entry in entries {
        match entry.key {
          Value::String(key) => object[key] = entry.value.to_json()
          other =>
            raise InvalidJsonValue(
              message="JSON output requires string map keys, found \{other.kind_name()}",
            )
        }
      }
      Json::object(object)
    }
  }
}

///|
pub fn Value::to_json_string(
  self : Value,
  indent? : Int = 2,
) -> String raise JsonBridgeError {
  self.to_json().stringify(indent~)
}

///|
fn split_map_types(body : String) -> (String, String)? {
  let mut depth = 0
  for index = 0; index < body.length(); index = index + 1 {
    match body[index] {
      '<' => depth = depth + 1
      '>' => depth = depth - 1
      ',' =>
        if depth == 0 {
          return Some((body[0:index].to_owned(), body[index + 1:].to_owned()))
        }
      _ => ()
    }
  }
  None
}

///|
/// Parse the compact type syntax accepted by the CLI type environment.
pub fn parse_type_name(name : String) -> CelType raise JsonBridgeError {
  let name = name.trim().to_owned()
  match name {
    "dyn" => CelType::Dyn
    "null" => CelType::Null
    "bool" => CelType::Bool
    "int" => CelType::Int
    "uint" => CelType::UInt
    "double" => CelType::Double
    "string" => CelType::String
    "bytes" => CelType::Bytes
    _ => {
      if name.has_prefix("list<") && name.has_suffix(">") {
        let body = name[5:name.length() - 1].to_owned()
        return CelType::List(parse_type_name(body))
      }
      if name.has_prefix("map<") && name.has_suffix(">") {
        let body = name[4:name.length() - 1].to_owned()
        match split_map_types(body) {
          Some((key, value)) =>
            return CelType::Map(parse_type_name(key), parse_type_name(value))
          None => ()
        }
      }
      raise InvalidTypeName(name~)
    }
  }
}

///|
pub fn type_env_from_json(json : Json) -> TypeEnv raise JsonBridgeError {
  match json {
    Json::Object(fields) => {
      let mut env = TypeEnv::new()
      for name, value in fields {
        match value {
          Json::String(type_name) =>
            env = env.with_variable(name, parse_type_name(type_name))
          _ =>
            raise InvalidJsonValue(
              message="type for '\{name}' must be a string",
            )
        }
      }
      env
    }
    _ =>
      raise InvalidJsonValue(message="type environment must be a JSON object")
  }
}

///|
pub fn activation_from_json(json : Json) -> Activation raise JsonBridgeError {
  match json {
    Json::Object(fields) => {
      let mut activation = Activation::new()
      for name, value in fields {
        activation = activation.bind(name, value_from_json(value))
      }
      activation
    }
    _ => raise InvalidJsonValue(message="activation must be a JSON object")
  }
}

///|
pub fn Activation::type_env(self : Activation) -> TypeEnv {
  let mut env = TypeEnv::new()
  for name, value in self.values {
    env = env.with_variable(name, value.cel_type())
  }
  env
}

///|
fn diagnostic_json(diagnostic : Diagnostic) -> Json {
  Json::object({
    "code": Json::string(diagnostic.code),
    "message": Json::string(diagnostic.message),
    "severity": Json::string(diagnostic.severity.label()),
    "span": Json::object({
      "start": Json::number(diagnostic.span.start.to_double()),
      "end": Json::number(diagnostic.span.end.to_double()),
      "line": Json::number(diagnostic.span.line.to_double()),
      "column": Json::number(diagnostic.span.column.to_double()),
    }),
  })
}

///|
pub fn diagnostics_to_json(diagnostics : Array[Diagnostic]) -> Json {
  Json::array(diagnostics.map(diagnostic_json))
}