///|
priv enum MaybeInput {
  InputProvided(InputValue)
  InputOmitted
}

///|
/// Convert a parsed GraphQL value into the runtime input representation before
/// schema-aware coercion is applied.
fn parser_value_to_input(
  value : @parser.Value,
  variables : Map[String, InputValue],
) -> InputValue raise CoercionError {
  match parser_value_to_maybe_input(value, variables) {
    InputProvided(value) => value
    InputOmitted => coercion_error("variable was not provided")
  }
}

///|
fn parser_value_to_maybe_input(
  value : @parser.Value,
  variables : Map[String, InputValue],
) -> MaybeInput raise CoercionError {
  match value {
    Variable(name) =>
      match variables.get(name) {
        Some(value) => InputProvided(value)
        None => InputOmitted
      }
    IntValue(number) => InputProvided(InputInt(number.as_int64()))
    FloatValue(value) => InputProvided(InputFloat(value))
    StringValue(value) => InputProvided(InputString(value))
    BooleanValue(value) => InputProvided(InputBoolean(value))
    NullValue => InputProvided(InputNull)
    EnumValue(value) => InputProvided(InputEnum(value))
    ListValue(values) => {
      let list : Array[InputValue] = []
      for item in values {
        match parser_value_to_maybe_input(item, variables) {
          InputProvided(value) => list.push(value)
          InputOmitted => list.push(InputNull)
        }
      }
      InputProvided(InputList(list))
    }
    ObjectValue(fields) => {
      let object : Map[String, InputValue] = Map([])
      for pair in fields {
        let name = pair.0
        let value = pair.1
        match parser_value_to_maybe_input(value, variables) {
          InputProvided(value) => object[name] = value
          InputOmitted => ()
        }
      }
      InputProvided(InputObject(object))
    }
  }
}

///|
fn default_value_to_input(
  value : @parser.Value,
) -> InputValue raise CoercionError {
  parser_value_to_input(value, {})
}

///|
/// Coerce a runtime input value according to a GraphQL input type.
///
/// Non-null, list, scalar, enum, and input-object semantics are handled here so
/// variables and field arguments follow the same rules.
fn coerce_input(
  registry : TypeRegistry,
  gql_type : @parser.GqlType,
  value : InputValue,
) -> InputValue raise CoercionError {
  match gql_type {
    NonNullType(inner) =>
      match value {
        InputNull =>
          coercion_error("expected non-null \{type_to_string(gql_type)}")
        _ => coerce_input(registry, inner, value)
      }
    ListType(inner) =>
      match value {
        InputNull => InputNull
        InputList(values) =>
          InputList(values.map(value => coerce_input(registry, inner, value)))
        _ => InputList([coerce_input(registry, inner, value)])
      }
    NamedType(name) =>
      match value {
        InputNull => InputNull
        _ => coerce_named_input(registry, name, value)
      }
  }
}

///|
fn coerce_named_input(
  registry : TypeRegistry,
  name : String,
  value : InputValue,
) -> InputValue raise CoercionError {
  match name {
    "String" =>
      match value {
        InputString(_) => value
        _ => coercion_error("expected String")
      }
    "ID" =>
      match value {
        InputString(_) | InputInt(_) => value
        _ => coercion_error("expected ID")
      }
    "Boolean" =>
      match value {
        InputBoolean(_) => value
        _ => coercion_error("expected Boolean")
      }
    "Int" =>
      match value {
        InputInt(_) => value
        _ => coercion_error("expected Int")
      }
    "Float" =>
      match value {
        InputFloat(_) | InputInt(_) => value
        _ => coercion_error("expected Float")
      }
    _ =>
      match registry.types.get(name) {
        Some(ScalarDef(_)) => value
        Some(EnumDef(values~, ..)) =>
          match value {
            InputEnum(enum_value) | InputString(enum_value) =>
              if values.contains(enum_value) {
                InputEnum(enum_value)
              } else {
                coercion_error("unknown enum value `\{name}.\{enum_value}`")
              }
            _ => coercion_error("expected enum `\{name}`")
          }
        Some(InputObjectDef(fields~, ..)) =>
          coerce_input_object(registry, name, fields, value)
        Some(_) => coercion_error("type `\{name}` is not an input type")
        None => coercion_error("unknown input type `\{name}`")
      }
  }
}

///|
fn coerce_input_object(
  registry : TypeRegistry,
  type_name : String,
  fields : Map[String, @parser.InputValue],
  value : InputValue,
) -> InputValue raise CoercionError {
  let object = match value {
    InputObject(object) => object
    _ => coercion_error("expected input object `\{type_name}`")
  }
  for name, _ in object.iter2() {
    if !fields.contains(name) {
      coercion_error("unknown field `\{type_name}.\{name}`")
    }
  }
  let coerced : Map[String, InputValue] = Map([])
  for name, field in fields.iter2() {
    match object.get(name) {
      Some(value) =>
        coerced[name] = coerce_input(registry, field.value_type, value)
      None =>
        match field.default_value {
          Some(default_value) =>
            coerced[name] = coerce_input(
              registry,
              field.value_type,
              default_value_to_input(default_value),
            )
          None =>
            if is_non_null(field.value_type) {
              coercion_error(
                "missing required input field `\{type_name}.\{name}`",
              )
            }
        }
    }
  }
  InputObject(coerced)
}

///|
/// Coerce field or directive arguments from parsed literal values plus already
/// coerced variable values.
fn coerce_argument_values(
  registry : TypeRegistry,
  definitions : Array[@parser.InputValue],
  values : Array[(String, @parser.Value)],
  variables : Map[String, InputValue],
) -> Args raise CoercionError {
  let provided : Map[String, @parser.Value] = Map::from_array(values)
  let result : Map[String, InputValue] = Map([])
  for name, _ in provided.iter2() {
    if !definitions.any(def => def.name == name) {
      coercion_error("unknown argument `\{name}`")
    }
  }
  for definition in definitions {
    match provided.get(definition.name) {
      Some(value) =>
        match parser_value_to_maybe_input(value, variables) {
          InputProvided(value) =>
            result[definition.name] = coerce_input(
              registry,
              definition.value_type,
              value,
            )
          InputOmitted =>
            apply_argument_default_or_required(registry, result, definition)
        }
      None => apply_argument_default_or_required(registry, result, definition)
    }
  }
  { values: result, }
}

///|
fn apply_argument_default_or_required(
  registry : TypeRegistry,
  result : Map[String, InputValue],
  definition : @parser.InputValue,
) -> Unit raise CoercionError {
  match definition.default_value {
    Some(default_value) =>
      result[definition.name] = coerce_input(
        registry,
        definition.value_type,
        default_value_to_input(default_value),
      )
    None =>
      if is_non_null(definition.value_type) {
        coercion_error("missing required argument `\{definition.name}`")
      }
  }
}

///|
/// Coerce operation variables against variable definitions before validation
/// and execution read them.
fn coerce_variable_values(
  registry : TypeRegistry,
  definitions : Array[@parser.VariableDefinition],
  provided : Map[String, InputValue],
) -> Map[String, InputValue] raise CoercionError {
  let variables : Map[String, InputValue] = Map([])
  for definition in definitions {
    match provided.get(definition.name) {
      Some(value) =>
        variables[definition.name] = coerce_input(
          registry,
          definition.var_type,
          value,
        )
      None =>
        match definition.default_value {
          Some(default_value) =>
            variables[definition.name] = coerce_input(
              registry,
              definition.var_type,
              default_value_to_input(default_value),
            )
          None =>
            if is_non_null(definition.var_type) {
              coercion_error("missing required variable `$\{definition.name}`")
            }
        }
    }
  }
  variables
}

///|
fn is_non_null(gql_type : @parser.GqlType) -> Bool {
  gql_type is NonNullType(_)
}

///|
fn type_to_string(gql_type : @parser.GqlType) -> String {
  match gql_type {
    NamedType(name) => name
    ListType(inner) => "[\{type_to_string(inner)}]"
    NonNullType(inner) => type_to_string(inner) + "!"
  }
}