///|
priv struct DocumentPointer {
  segments : Array[String]
}

///|
fn DocumentPointer::parse(text : String) -> DocumentPointer raise OpenApiError {
  let pointer = text.trim_start(chars="#").to_owned()
  if pointer.is_empty() {
    return { segments: [], }
  }
  guard pointer.has_prefix("/") else {
    raise InvalidConfiguration("JSON pointer must start with /: " + text)
  }
  let segments = pointer[1:]
    .split("/")
    .map(part => {
      part
      .to_owned()
      .replace_all(old="~1", new="/")
      .replace_all(old="~0", new="~")
    })
    .to_array()
  { segments, }
}

///|
fn substitute_json(node : Json, substitutions : Map[String, Json]) -> Json {
  match node {
    String(value) => substitutions.get(value).unwrap_or(node)
    Array(values) =>
      Json::array(values.map(value => substitute_json(value, substitutions)))
    Object(properties) => {
      let out : Map[String, Json] = Map([])
      for key, value in properties {
        out[key] = substitute_json(value, substitutions)
      }
      Json::object(out)
    }
    _ => node
  }
}

///|
fn add_json_value(
  node : Json,
  segments : Array[String],
  at : Int32,
  value : Json,
) -> Json {
  if at == segments.length() {
    return value
  }
  let key = segments[at]
  let last = at == segments.length() - 1
  match node {
    Object(properties) => {
      let out = copy_fields(node)
      out[key] = if last {
        value
      } else {
        add_json_value(
          properties.get(key).unwrap_or(Json::object(Map([]))),
          segments,
          at + 1,
          value,
        )
      }
      Json::object(out)
    }
    Array(values) => {
      let index = if key == "-" {
        if last {
          values.length()
        } else {
          values.length() - 1
        }
      } else {
        @string.parse_int(key) catch {
          _ => -1
        }
      }
      if index < 0 ||
        index > values.length() ||
        (!last && index == values.length()) {
        return node
      }
      let out : Array[Json] = []
      for i = 0; i < values.length(); i = i + 1 {
        if i == index && last {
          out.push(value)
        }
        out.push(
          if i == index && !last {
            add_json_value(values[i], segments, at + 1, value)
          } else {
            values[i]
          },
        )
      }
      if last && index == values.length() {
        out.push(value)
      }
      Json::array(out)
    }
    _ => node
  }
}

///|
fn apply_json_settings(
  document : Json,
  config : Config,
) -> Json raise OpenApiError {
  let mut result = if config.substitutions.is_empty() {
    document
  } else {
    substitute_json(document, config.substitutions)
  }
  for pointer, value in config.json_add {
    let pointer = DocumentPointer::parse(pointer)
    result = add_json_value(result, pointer.segments, 0, value)
  }
  guard result is Object(_) else {
    raise InvalidConfiguration(
      "OpenAPI jsonAdd must preserve an object document",
    )
  }
  result
}