///|
fn json_object(
  value : Json,
  path : String,
) -> Map[String, Json] raise SourceMapError {
  match value {
    Object(fields) => fields
    _ => raise InvalidField(path~, message="expected an object")
  }
}

///|
fn json_array(value : Json, path : String) -> Array[Json] raise SourceMapError {
  match value {
    Array(values) => values
    _ => raise InvalidField(path~, message="expected an array")
  }
}

///|
fn json_string(value : Json, path : String) -> String raise SourceMapError {
  match value {
    String(text) => text
    _ => raise InvalidField(path~, message="expected a string")
  }
}

///|
fn json_int(value : Json, path : String) -> Int raise SourceMapError {
  match value {
    Number(number, ..) =>
      if number != number.floor() {
        raise InvalidField(path~, message="expected an integer")
      } else {
        number.to_int()
      }
    _ => raise InvalidField(path~, message="expected an integer")
  }
}

///|
fn required_json(
  fields : Map[String, Json],
  key : String,
  path : String,
) -> Json raise SourceMapError {
  match fields.get(key) {
    Some(value) => value
    None =>
      raise InvalidField(
        path="\{path}.\{key}",
        message="missing required field",
      )
  }
}

///|
fn optional_string(
  fields : Map[String, Json],
  key : String,
  path : String,
) -> String? raise SourceMapError {
  match fields.get(key) {
    None | Some(Null) => None
    Some(value) => Some(json_string(value, "\{path}.\{key}"))
  }
}

///|
fn string_array(
  value : Json,
  path : String,
  allow_null : Bool,
) -> Array[String?] raise SourceMapError {
  json_array(value, path).mapi((index, item) => {
    match item {
      Null if allow_null => None
      String(text) => Some(text)
      _ =>
        raise InvalidField(
          path="\{path}[\{index}]",
          message=if allow_null {
            "expected a string or null"
          } else {
            "expected a string"
          },
        )
    }
  })
}

///|
fn optional_nullable_strings(
  fields : Map[String, Json],
  key : String,
  path : String,
) -> Array[String?]? raise SourceMapError {
  match fields.get(key) {
    None | Some(Null) => None
    Some(value) => Some(string_array(value, "\{path}.\{key}", true))
  }
}

///|
fn optional_int_array(
  fields : Map[String, Json],
  key : String,
  path : String,
) -> Array[Int] raise SourceMapError {
  match fields.get(key) {
    None | Some(Null) => []
    Some(value) =>
      json_array(value, "\{path}.\{key}").mapi((index, item) => {
        json_int(item, "\{path}.\{key}[\{index}]")
      })
  }
}

///|
fn parse_regular(
  fields : Map[String, Json],
  path : String,
) -> RegularSourceMap raise SourceMapError {
  if fields.contains("sections") {
    raise InvalidField(
      path="\{path}.sections",
      message="regular maps cannot contain sections",
    )
  }
  let version = json_int(
    required_json(fields, "version", path),
    "\{path}.version",
  )
  let sources = string_array(
    required_json(fields, "sources", path),
    "\{path}.sources",
    true,
  )
  let names = match fields.get("names") {
    None => []
    Some(value) =>
      string_array(value, "\{path}.names", false).map(item => {
        match item {
          Some(name) => name
          None => ""
        }
      })
  }
  let ignore_list = if fields.contains("ignoreList") {
    optional_int_array(fields, "ignoreList", path)
  } else {
    optional_int_array(fields, "x_google_ignoreList", path)
  }
  {
    version,
    file: optional_string(fields, "file", path),
    source_root: optional_string(fields, "sourceRoot", path),
    sources,
    sources_content: optional_nullable_strings(fields, "sourcesContent", path),
    names,
    mappings: json_string(
      required_json(fields, "mappings", path),
      "\{path}.mappings",
    ),
    ignore_list,
  }
}

///|
fn parse_offset(value : Json, path : String) -> Position raise SourceMapError {
  let fields = json_object(value, path)
  Position::new(
    line=json_int(required_json(fields, "line", path), "\{path}.line"),
    column=json_int(required_json(fields, "column", path), "\{path}.column"),
  )
}

///|
fn parse_index(
  fields : Map[String, Json],
  path : String,
) -> IndexSourceMap raise SourceMapError {
  for
    forbidden in [
      "mappings", "sources", "names", "sourceRoot", "sourcesContent",
    ] {
    if fields.contains(forbidden) {
      raise InvalidField(
        path="\{path}.\{forbidden}",
        message="index maps cannot contain regular-map fields",
      )
    }
  }
  let sections_json = json_array(
    required_json(fields, "sections", path),
    "\{path}.sections",
  )
  let sections = sections_json.mapi((index, section_json) => {
    let section_path = "\{path}.sections[\{index}]"
    let section = json_object(section_json, section_path)
    if section.contains("url") {
      raise InvalidField(
        path="\{section_path}.url",
        message="external section URLs are not supported in v0.1",
      )
    }
    IndexSection::new(
      offset=parse_offset(
        required_json(section, "offset", section_path),
        "\{section_path}.offset",
      ),
      map=parse_document_json(
        required_json(section, "map", section_path),
        "\{section_path}.map",
      ),
    )
  })
  {
    version: json_int(required_json(fields, "version", path), "\{path}.version"),
    file: optional_string(fields, "file", path),
    sections,
  }
}

///|
fn parse_document_json(
  value : Json,
  path : String,
) -> SourceMapDocument raise SourceMapError {
  let fields = json_object(value, path)
  if fields.contains("sections") {
    Indexed(parse_index(fields, path))
  } else {
    Regular(parse_regular(fields, path))
  }
}

///|
/// Parse a regular or index source map JSON document.
pub fn parse_document(
  input : StringView,
) -> SourceMapDocument raise SourceMapError {
  let value = @json.parse(input) catch {
    error => raise InvalidJson(message=error.to_string())
  }
  parse_document_json(value, "$")
}