///|
fn json_error(message : String) -> SourceMapError {
  SourceMapError::At(0, message)
}

///|
fn required_field(
  object : Map[String, Json],
  name : String,
) -> Json raise SourceMapError {
  match object.get(name) {
    Some(value) => value
    None => raise json_error("missing required field '" + name + "'")
  }
}

///|
fn required_string(
  object : Map[String, Json],
  name : String,
) -> String raise SourceMapError {
  match required_field(object, name) {
    Json::String(value) => value
    _ => raise json_error("field '" + name + "' must be a string")
  }
}

///|
fn optional_string(
  object : Map[String, Json],
  name : String,
) -> String? raise SourceMapError {
  match object.get(name) {
    None => None
    Some(Json::String(value)) => Some(value)
    Some(_) => raise json_error("field '" + name + "' must be a string")
  }
}

///|
fn string_array(
  value : Json,
  name : String,
) -> Array[String] raise SourceMapError {
  match value {
    Json::Array(values) => {
      let result : Array[String] = []
      for index, value in values {
        match value {
          Json::String(item) => result.push(item)
          _ =>
            raise json_error(
              "field '" +
              name +
              "' item " +
              index.to_string() +
              " must be a string",
            )
        }
      }
      result
    }
    _ => raise json_error("field '" + name + "' must be an array")
  }
}

///|
fn optional_sources_content(
  object : Map[String, Json],
) -> Array[String?] raise SourceMapError {
  match object.get("sourcesContent") {
    None => []
    Some(Json::Array(values)) => {
      let result : Array[String?] = []
      for index, value in values {
        match value {
          Json::String(content) => result.push(Some(content))
          Json::Null => result.push(None)
          _ =>
            raise json_error(
              "field 'sourcesContent' item " +
              index.to_string() +
              " must be a string or null",
            )
        }
      }
      result
    }
    Some(_) => raise json_error("field 'sourcesContent' must be an array")
  }
}

///|
/// Parse a regular (non-indexed) Source Map v3 JSON document.
pub fn parse_json(input : String) -> SourceMap raise SourceMapError {
  let document = @json.parse(input) catch {
    error => raise json_error("invalid JSON: " + error.to_string())
  }
  let object = match document {
    Json::Object(value) => value
    _ => raise json_error("Source Map JSON root must be an object")
  }
  if object.get("sections") is Some(_) {
    raise json_error("Index Map 'sections' are not supported")
  }
  match required_field(object, "version") {
    Json::Number(version, ..) if version == 3.0 => ()
    Json::Number(_, ..) => raise json_error("field 'version' must equal 3")
    _ => raise json_error("field 'version' must be the number 3")
  }
  let sources = string_array(required_field(object, "sources"), "sources")
  let names = string_array(required_field(object, "names"), "names")
  let encoded = required_string(object, "mappings")
  let file = optional_string(object, "file")
  let source_root = optional_string(object, "sourceRoot")
  let sources_content = optional_sources_content(object)
  let decoded = parse_mappings(encoded, sources, names)
  SourceMap::make(
    decoded.sources,
    decoded.names,
    decoded.mappings,
    file~,
    source_root~,
    sources_content~,
  )
}

///|
fn strings_to_json(values : Array[String]) -> Json {
  Json::array(values.map(value => Json::string(value)))
}

///|
fn sources_content_to_json(values : Array[String?]) -> Json {
  Json::array(
    values.map(value => {
      match value {
        Some(content) => Json::string(content)
        None => Json::null()
      }
    }),
  )
}

///|
/// Serialize a regular Source Map v3 document as compact or indented JSON.
pub fn SourceMap::to_json(self : SourceMap, pretty? : Bool = false) -> String {
  let object : Map[String, Json] = Map([])
  object.set("version", Json::number(3.0, repr="3"))
  object.set("sources", strings_to_json(self.sources))
  object.set("names", strings_to_json(self.names))
  object.set("mappings", Json::string(self.encode_mappings()))
  match self.file {
    Some(file) => object.set("file", Json::string(file))
    None => ()
  }
  match self.source_root {
    Some(root) => object.set("sourceRoot", Json::string(root))
    None => ()
  }
  if self.sources_content.length() > 0 {
    object.set("sourcesContent", sources_content_to_json(self.sources_content))
  }
  let document = Json::object(object)
  if pretty {
    document.stringify(indent=2)
  } else {
    document.stringify()
  }
}