/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::fmt::Display;

use common::ArgumentName;
use common::DiagnosticDisplay;
use common::DirectiveName;
use common::WithDiagnosticData;
use graphql_syntax::OperationKind;
use intern::string_key::StringKey;
use intern::Lookup;
use schema::suggestion_list::did_you_mean;
use schema::Type;
use schema::TypeReference;
use thiserror::Error;

use crate::ir::FragmentDefinitionName;
use crate::VariableName;

struct ErrorLink(&'static str);

impl Display for ErrorLink {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f)?;
        write!(f, "See https://relay.dev/docs/error-reference/{}/", self.0)
    }
}

/// Fixed set of validation errors with custom display messages
#[derive(Clone, Debug, Error, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum ValidationMessage {
    #[error("Duplicate definitions for '{0}'")]
    DuplicateDefinition(StringKey),

    #[error("Expected an object, interface, or union, found '{0:?}'")]
    ExpectedCompositeType(Type),

    #[error("Expected type '{0:?}")]
    ExpectedType(TypeReference<Type>),

    #[error("Expected no selections on scalar field `{field_name}` of type `{type_name}`")]
    InvalidSelectionsOnScalarField {
        field_name: StringKey,
        type_name: StringKey,
    },

    #[error("Unknown directive '{0}'")]
    UnknownDirective(DirectiveName),

    #[error(
        "Invalid use of @uncheckedArguments_DEPRECATED: all arguments are defined and of correct type, use @arguments instead."
    )]
    UnnecessaryUncheckedArgumentsDirective,

    #[error("Expected operation to have a name (e.g. 'query <Name>')")]
    ExpectedOperationName,

    #[error("The schema does not support '{0}' operations")]
    UnsupportedOperation(OperationKind),

    #[error("Nested lists ('[[T]]' etc) are not supported")]
    UnsupportedNestListType,

    #[error("Expected a value of type '{0}'")]
    ExpectedValueMatchingType(StringKey),

    #[error(
        "Expected value of type '{0}' to be a valid enum value, got string. Consider removing quotes."
    )]
    ExpectedEnumValueGotString(StringKey),

    #[error("Duplicate values found for field '{0}'")]
    DuplicateInputField(StringKey),

    #[error("Missing required fields '{0:?}' of type '{1}'")] // TODO: print joined
    MissingRequiredFields(Vec<StringKey>, StringKey),

    #[error("Unsupported (user-defined) scalar type '{0}'")]
    UnsupportedCustomScalarType(StringKey),

    #[error("Expected at-most one '@arguments' directive per fragment spread")]
    ExpectedOneArgumentsDirective,

    #[error("Expected at-most one '@argumentDefinitions' directive per fragment spread")]
    ExpectedOneArgumentDefinitionsDirective,

    #[error(
        "Cannot combine fragment variable definitions syntax with the '@argumentDefinitions' directive"
    )]
    VariableDefinitionsAndArgumentDirective,

    #[error("Cannot combine fragment arguments syntax with the '@arguments' directive")]
    FragmentArgumentsAndArgumentDirective,

    #[error("Unexpected fragment argument. Fragment argument syntax is not enabled.")]
    OutsidePassedArgumentsMode,

    #[error(
        "Expected `@argumentDefinitions` value to have a `type` field with a literal string value (e.g. `type: \"Int!\"`)"
    )]
    ExpectedArgumentDefinitionLiteralType,

    #[error(
        "Expected `@argumentDefinitions` value to be an object with `type` and (optionally) `defaultValue` properties"
    )]
    ExpectedArgumentDefinitionToBeObject,

    #[error("Expected '@argumentDefinitions' directive to be used on fragment definitions only.")]
    ExpectedArgumentDefinitionsDirectiveOnFragmentDefinition,

    #[error(
        "Expected the `directives` argument to `@argumentDefinition` to be a list of literal strings in the form `directives: [\"@example\"]`."
    )]
    ArgumentDefinitionsDirectivesNotStringListLiteral,

    #[error("Non-nullable variable '{variable_name}' has a default value.")]
    NonNullableVariableHasDefaultValue { variable_name: VariableName },

    #[error(
        "Variable was defined as type '{defined_type}' but used where a variable of type '{used_type}' is expected."
    )]
    InvalidVariableUsage {
        defined_type: String,
        used_type: String,
    },

    #[error(
        "Variable was previously used as type '{prev_type}' but later used where type '{next_type}' is expected."
    )]
    IncompatibleVariableUsage {
        prev_type: String,
        next_type: String,
    },

    #[error("Expected variable `${0}` to be defined on the operation")]
    ExpectedOperationVariableToBeDefined(VariableName),

    #[error(
        "Expected argument definition to have an input type (scalar, enum, or input object), found type '{0}'"
    )]
    ExpectedFragmentArgumentToHaveInputType(StringKey),

    #[error(
        "Expected variable definition to have an input type (scalar, enum, or input object), found type '{0}'"
    )]
    ExpectedVariablesToHaveInputType(StringKey),

    #[error(
        "Invalid type '{type_condition}' in inline fragment, this type can never occur for parent type '{parent_type}'"
    )]
    InvalidInlineFragmentTypeCondition {
        parent_type: StringKey,
        type_condition: StringKey,
    },

    #[error(
        "Invalid fragment spread '{fragment_name}', the type of this fragment ('{type_condition}') can never occur for parent type '{parent_type}'"
    )]
    InvalidFragmentSpreadType {
        fragment_name: FragmentDefinitionName,
        parent_type: StringKey,
        type_condition: StringKey,
    },

    #[error("Directive '{0}' not supported in this location")]
    InvalidDirectiveUsageUnsupportedLocation(DirectiveName),

    #[error(
        "Invalid value passed to `@argumentDefinitions`, supported options include `type` and `defaultValue`, got `{0}`"
    )]
    InvalidArgumentDefinitionsKey(StringKey),

    #[error("Unexpected arguments on `__typename` field")]
    InvalidArgumentsOnTypenameField,

    #[error("Unexpected arguments on '__token' field")]
    InvalidArgumentsOnFetchTokenField,

    #[error(
        "Invalid type `{id_type_string}` of field `{id_field_name}` on parent type `{parent_type_name}`. Fields named `{id_field_name}` can only have `ID` or `String`-like types (e.g. custom scalars or enums)."
    )]
    InvalidIdFieldType {
        parent_type_name: StringKey,
        id_field_name: StringKey,
        id_type_string: String,
    },

    #[error(
        "Disallowed type `{id_type_string}` of field `{id_field_name}` on parent type `{parent_type_name}` cannot be used by Relay to identify entities. For a detailed explanation, check out https://relay.dev/docs/debugging/disallowed-id-types-error"
    )]
    DisallowNonNodeIdFieldType {
        parent_type_name: StringKey,
        id_field_name: StringKey,
        id_type_string: String,
    },

    #[error("Relay does not allow aliasing fields to `{0}`.")]
    DisallowReservedAliasError(StringKey),

    #[error("Relay does not allow `__typename` field on Query, Mutation or Subscription.")]
    DisallowTypenameOnRoot,

    #[error(
        "Unexpected directive: '{0}'. This directive can only be used on fields/fragments that are fetched from the server schema, but it is used inside a client-only selection."
    )]
    InvalidServerOnlyDirectiveInClientFields(DirectiveName),

    #[error(
        "@{connection_directive_name} used on invalid field '{connection_field_name}'. Expected the return type to be a non-plural interface or object, got '{connection_type_string}'."
    )]
    InvalidConnectionFieldType {
        connection_directive_name: DirectiveName,
        connection_field_name: StringKey,
        connection_type_string: String,
    },

    #[error(
        "Expected field '{connection_field_name}' to have a '{first_arg}' or '{last_arg}' argument."
    )]
    ExpectedConnectionToHaveCountArgs {
        connection_field_name: StringKey,
        first_arg: ArgumentName,
        last_arg: ArgumentName,
    },

    #[error("Expected '{connection_field_name}' to have a '{edges_selection_name}' selection.")]
    ExpectedConnectionToHaveEdgesSelection {
        connection_field_name: StringKey,
        edges_selection_name: StringKey,
    },

    #[error(
        "@{connection_directive_name} used on invalid field '{connection_field_name}'. Expected the field type '{connection_type_name}' to expose a '{edges_selection_name}' field that returns a list of objects."
    )]
    ExpectedConnectionToExposeValidEdgesField {
        connection_directive_name: DirectiveName,
        connection_field_name: StringKey,
        connection_type_name: StringKey,
        edges_selection_name: StringKey,
    },

    #[error(
        "@{connection_directive_name} used on invalid field '{connection_field_name}'. Expected the field type '{connection_type_name}' to expose a '{edges_selection_name} {{ {node_selection_name} }}' field that returns an object, interface or union."
    )]
    ExpectedConnectionToExposeValidNodeField {
        connection_directive_name: DirectiveName,
        connection_field_name: StringKey,
        connection_type_name: StringKey,
        edges_selection_name: StringKey,
        node_selection_name: StringKey,
    },

    #[error(
        "@{connection_directive_name} used on invalid field '{connection_field_name}'. Expected the field type '{connection_type_name}' to expose a '{edges_selection_name} {{ {cursor_selection_name} }}' field that returns a scalar."
    )]
    ExpectedConnectionToExposeValidCursorField {
        connection_directive_name: DirectiveName,
        connection_field_name: StringKey,
        connection_type_name: StringKey,
        cursor_selection_name: StringKey,
        edges_selection_name: StringKey,
    },

    #[error(
        "@{connection_directive_name} used on invalid field '{connection_field_name}'. Expected the field type '{connection_type_name}' to expose a '{page_info_selection_name}' field that returns an object."
    )]
    ExpectedConnectionToExposeValidPageInfoField {
        connection_directive_name: DirectiveName,
        connection_field_name: StringKey,
        connection_type_name: StringKey,
        page_info_selection_name: StringKey,
    },

    #[error(
        "@{connection_directive_name} used on invalid field '{connection_field_name}'. Expected the field type '{connection_type_name}' to expose a '{page_info_selection_name} {{ {page_info_sub_field_name} }}' field that returns a scalar."
    )]
    ExpectedConnectionToExposeValidPageInfoSubField {
        connection_directive_name: DirectiveName,
        connection_field_name: StringKey,
        connection_type_name: StringKey,
        page_info_selection_name: StringKey,
        page_info_sub_field_name: StringKey,
    },

    #[error(
        "Expected the {handler_arg_name} argument to @{connection_directive_name} to be a string literal for field '{connection_field_name}'."
    )]
    InvalidConnectionHandlerArg {
        connection_directive_name: DirectiveName,
        connection_field_name: StringKey,
        handler_arg_name: ArgumentName,
    },

    #[error(
        "Expected the {key_arg_name} argument to @{connection_directive_name} to be a string literal for field '{connection_field_name}'."
    )]
    InvalidConnectionKeyArg {
        connection_directive_name: DirectiveName,
        connection_field_name: StringKey,
        key_arg_name: ArgumentName,
    },

    #[error(
        "Expected the {dynamic_key_arg_name} argument to @{connection_directive_name} to be a variable for field '{connection_field_name}'."
    )]
    InvalidConnectionDynamicKeyArg {
        connection_directive_name: DirectiveName,
        connection_field_name: StringKey,
        dynamic_key_arg_name: ArgumentName,
    },

    #[error(
        "Expected the {key_arg_name} argument to @{connection_directive_name} to be of form '<SomeName>_{postfix}', got '{key_arg_value}'. For a detailed explanation, check out https://relay.dev/docs/en/pagination-container#connection"
    )]
    InvalidConnectionKeyArgPostfix {
        connection_directive_name: DirectiveName,
        connection_field_name: StringKey,
        key_arg_name: ArgumentName,
        key_arg_value: StringKey,
        postfix: String,
    },

    #[error(
        "Expected the {filters_arg_name} argument to @{connection_directive_name} to be a list of string literals for field '{connection_field_name}'."
    )]
    InvalidConnectionFiltersArg {
        connection_directive_name: DirectiveName,
        connection_field_name: StringKey,
        filters_arg_name: ArgumentName,
    },

    #[error(
        "Expected the `{filters_arg_name}` argument to `@{connection_directive_name}` to be a list of argument names to the connection field to use to identify the connection, got `{invalid_name}`. Not specifying `filters` is often recommended and will use all fields."
    )]
    InvalidConnectionFiltersArgNotAnArgument {
        connection_directive_name: DirectiveName,
        connection_field_name: StringKey,
        filters_arg_name: ArgumentName,
        invalid_name: StringKey,
    },

    #[error("@stream_connection does not support aliasing the '{field_name}' field.")]
    UnsupportedAliasingInStreamConnection { field_name: StringKey },

    #[error("Expected the `{0}` argument to @relay to be a boolean literal if specified.")]
    InvalidRelayDirectiveArg(ArgumentName),

    #[error("Cannot use @relay(mask: false) on fragment spreads for fragments with directives.")]
    InvalidUnmaskOnFragmentWithDirectives,

    #[error(
        "Cannot use @relay(mask: false) on fragment spreads for fragments with @argumentDefinitions."
    )]
    InvalidUnmaskOnFragmentWithArguments,

    #[error("Cannot combine global and local variables when applying @relay(mask: false")]
    InvalidUnmaskOnLocalAndGloablVariablesWithSameName,

    #[error(
        "Cannot combine variables with incompatible types {prev_arg_type} and {next_arg_type} when applying @relay(mask: false"
    )]
    InvalidUnmaskOnVariablesOfIncompatibleTypesWithSameName {
        prev_arg_type: String,
        next_arg_type: String,
    },

    #[error(
        "Expected the '{arg_name}' value to @{directive_name} to be a string literal if provided."
    )]
    LiteralStringArgumentExpectedForDirective {
        arg_name: ArgumentName,
        directive_name: DirectiveName,
    },

    #[error("Variable `${variable_name}` is never used in operation `{operation_name}`")]
    UnusedVariable {
        variable_name: VariableName,
        operation_name: StringKey,
    },

    #[error(
        "Variable `${variable_name}` is never used in fragment `{fragment_name}`. `@argumentDefinitions` defines local variables, global variables are implicitly available."
    )]
    UnusedFragmentVariable {
        variable_name: VariableName,
        fragment_name: FragmentDefinitionName,
    },

    #[error(
        "Variable `${variable_name}` of fragment `{fragment_name}` is marked as unused using `unusedLocalVariable_DEPRECATED: true`, but is actually used. `unusedLocalVariable_DEPRECATED: true` should be removed."
    )]
    UselessUnusedFragmentVariableAnnotation {
        variable_name: VariableName,
        fragment_name: FragmentDefinitionName,
    },

    #[error(
        "`unusedLocalVariable_DEPRECATED` can only be set to a constant `true` value. Remove the `unusedLocalVariable_DEPRECATED` or update the value."
    )]
    InvalidUnusedFragmentVariableSuppressionArg,

    #[error(
        "Invalid usage of '@DEPRECATED__relay_ignore_unused_variables_error'. No unused variables found in the query '{operation_name}'."
    )]
    UnusedIgnoreUnusedVariablesDirective { operation_name: StringKey },

    #[error("Operation '{operation_name}' references undefined variable{variables_string}.")]
    GlobalVariables {
        operation_name: StringKey,
        variables_string: String,
    },

    #[error("Subscription '{subscription_name}' must have a single selection")]
    GenerateSubscriptionNameSingleSelectionItem { subscription_name: StringKey },

    #[error("The directive `@{name}` can only be used once at this location.")]
    RepeatedNonRepeatableDirective { name: DirectiveName },

    #[error("Module-provided variable ('{argument_name}') may not declare a default value")]
    ProvidedVariableIncompatibleWithDefaultValue { argument_name: StringKey },

    #[error("The field `{parent_name}.{field_name}` is deprecated.{}",
        match deprecation_reason {
            Some(reason) => format!(" Deprecation reason: \"{}\"", reason),
            None => "".to_string()
        }
    )]
    DeprecatedField {
        parent_name: StringKey,
        field_name: StringKey,
        deprecation_reason: Option<StringKey>,
    },

    #[error("The argument `{argument_name}` of the field `{parent_name}.{field_name}` is deprecated.{}",
    match deprecation_reason {
        Some(reason) => format!(" Deprecation reason: \"{}\"", reason),
        None => "".to_string()
    })]
    DeprecatedFieldArgument {
        argument_name: ArgumentName,
        parent_name: StringKey,
        field_name: StringKey,
        deprecation_reason: Option<StringKey>,
    },

    #[error("The argument `{argument_name}` of the directive `@{directive_name}` is deprecated.{}",
    match deprecation_reason {
        Some(reason) => format!(" Deprecation reason: \"{}\"", reason),
        None => "".to_string()
    })]
    DeprecatedDirectiveArgument {
        argument_name: ArgumentName,
        directive_name: DirectiveName,
        deprecation_reason: Option<StringKey>,
    },

    #[error("Missing required {}: `{}`",
        if missing_arg_names.len() > 1 { "arguments" } else { "argument" },
        missing_arg_names
            .iter()
            .map(|arg| arg.lookup())
            .collect::<Vec<_>>()
            .join("`, `"))
    ]
    MissingRequiredArguments { missing_arg_names: Vec<StringKey> },

    #[error("Duplicate argument `{name}`")]
    DuplicateArgument { name: StringKey },

    #[error(
        "Required argument '{argument_name}: {type_string}' is missing on '{node_name}' in '{root_name}'."
    )]
    MissingRequiredArgument {
        argument_name: ArgumentName,
        type_string: String,
        node_name: StringKey,
        root_name: StringKey,
    },

    #[error("Missing required argument `{argument_name}` on this fragment spread.")]
    MissingRequiredFragmentArgument { argument_name: StringKey },

    #[error("Duplicate variable `{name}`")]
    DuplicateVariable { name: StringKey },

    #[error(
        "The `raw_response_type` argument should be set to `true` for the @no_inline fragment `{fragment_name}` used in the query with @raw_response_type."
    )]
    RequiredRawResponseTypeOnNoInline {
        fragment_name: FragmentDefinitionName,
    },

    #[error("No fields can have an alias that start with two underscores.")]
    NoDoubleUnderscoreAlias,
}

#[derive(Clone, Debug, Error, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum ValidationMessageWithData {
    #[error("Unknown type '{type_name}'.{suggestions}", suggestions = did_you_mean(suggestions))]
    UnknownType {
        type_name: StringKey,
        suggestions: Vec<StringKey>,
    },

    #[error("The type `{type_}` has no field `{field}`.{suggestions}{error_link}",
        suggestions = did_you_mean(suggestions),
        error_link = ErrorLink("unknown-field"))]
    UnknownField {
        type_: StringKey,
        field: StringKey,
        suggestions: Vec<StringKey>,
    },

    #[error("Expected selections on field `{field_name}` of type `{type_name}`")]
    ExpectedSelectionsOnObjectField {
        field_name: StringKey,
        type_name: StringKey,
    },

    #[error("Undefined fragment '{fragment_name}'.{suggestions}", suggestions = did_you_mean(suggestions))]
    UndefinedFragment {
        fragment_name: FragmentDefinitionName,
        suggestions: Vec<StringKey>,
    },

    #[error("Unknown argument '{argument_name}'.{suggestions}", suggestions = did_you_mean(suggestions))]
    UnknownArgument {
        argument_name: StringKey,
        suggestions: Vec<StringKey>,
    },
}

impl WithDiagnosticData for ValidationMessageWithData {
    fn get_data(&self) -> Vec<Box<dyn DiagnosticDisplay>> {
        match self {
            ValidationMessageWithData::UnknownArgument { suggestions, .. }
            | ValidationMessageWithData::UnknownType { suggestions, .. }
            | ValidationMessageWithData::UnknownField { suggestions, .. }
            | ValidationMessageWithData::UndefinedFragment { suggestions, .. } => suggestions
                .iter()
                .map(|suggestion| into_box(*suggestion))
                .collect::<_>(),
            ValidationMessageWithData::ExpectedSelectionsOnObjectField { field_name, .. } => {
                vec![Box::new(format!("{} {{ }}", field_name))]
            }
        }
    }
}

fn into_box(item: StringKey) -> Box<dyn DiagnosticDisplay> {
    Box::new(item)
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt;
use std::fmt::Display;
use std::fmt::Formatter;
use std::hash::Hash;
use std::str::FromStr;
use std::sync::Arc;

use common::ArgumentName;
use common::DirectiveName;
use common::Location;
use common::Named;
use common::WithLocation;
use graphql_syntax::FloatValue;
use graphql_syntax::OperationKind;
use intern::impl_lookup;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use intern::BuildIdHasher;
use intern::Lookup;
use schema::FieldID;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;
use schema::TypeReference;
use serde::Deserialize;
use serde::Serialize;

use crate::AssociatedData;
// Definitions

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ExecutableDefinition {
    Operation(OperationDefinition),
    Fragment(FragmentDefinition),
}

impl ExecutableDefinition {
    pub fn has_directive(&self, directive_name: DirectiveName) -> bool {
        match self {
            ExecutableDefinition::Operation(node) => node
                .directives
                .iter()
                .any(|d| d.name.item == directive_name),
            ExecutableDefinition::Fragment(node) => node
                .directives
                .iter()
                .any(|d| d.name.item == directive_name),
        }
    }

    pub fn name_with_location(&self) -> WithLocation<StringKey> {
        match self {
            ExecutableDefinition::Operation(node) => node.name.map(|x| x.0),
            ExecutableDefinition::Fragment(node) => node.name.map(|x| x.0),
        }
    }
}
#[derive(
    Copy,
    Clone,
    Debug,
    Eq,
    PartialEq,
    Ord,
    PartialOrd,
    Hash,
    Serialize,
    Deserialize
)]
pub struct OperationDefinitionName(pub StringKey);

impl Display for OperationDefinitionName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}
impl_lookup!(OperationDefinitionName);
/// A fully-typed mutation, query, or subscription definition
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OperationDefinition {
    pub kind: OperationKind,
    pub name: WithLocation<OperationDefinitionName>,
    pub type_: Type,
    pub variable_definitions: Vec<VariableDefinition>,
    pub directives: Vec<Directive>,
    pub selections: Vec<Selection>,
}

impl Named for OperationDefinition {
    type Name = OperationDefinitionName;
    fn name(&self) -> OperationDefinitionName {
        self.name.item
    }
}

impl OperationDefinition {
    pub fn is_query(&self) -> bool {
        self.kind == OperationKind::Query
    }
    pub fn is_mutation(&self) -> bool {
        self.kind == OperationKind::Mutation
    }
    pub fn is_subscription(&self) -> bool {
        self.kind == OperationKind::Subscription
    }
}

/// A newtype wrapper around StringKey to represent a FragmentDefinition's name
#[derive(
    Copy,
    Clone,
    Debug,
    Eq,
    PartialEq,
    Hash,
    Ord,
    PartialOrd,
    Serialize,
    Deserialize
)]
pub struct FragmentDefinitionName(pub StringKey);

impl fmt::Display for FragmentDefinitionName {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

impl_lookup!(FragmentDefinitionName);

pub type FragmentDefinitionNameMap<V> = HashMap<FragmentDefinitionName, V, BuildIdHasher<u32>>;
pub type FragmentDefinitionNameSet = HashSet<FragmentDefinitionName, BuildIdHasher<u32>>;

/// A fully-typed fragment definition
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FragmentDefinition {
    pub name: WithLocation<FragmentDefinitionName>,
    pub variable_definitions: Vec<VariableDefinition>,
    pub used_global_variables: Vec<VariableDefinition>,
    pub type_condition: Type,
    pub directives: Vec<Directive>,
    pub selections: Vec<Selection>,
}

/// An enum that can contain an operation definition name (e.g. names of queries,
/// subscriptions and mutations) or a fragment name. Note that the graphql spec
/// [defines](https://spec.graphql.org/draft/#ExecutableDefinition) executable
/// definitions as either operations or fragments.
#[derive(
    Copy,
    Clone,
    Debug,
    Eq,
    Hash,
    PartialEq,
    Ord,
    PartialOrd,
    Serialize,
    Deserialize
)]
pub enum ExecutableDefinitionName {
    OperationDefinitionName(OperationDefinitionName),
    FragmentDefinitionName(FragmentDefinitionName),
}

impl fmt::Display for ExecutableDefinitionName {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            ExecutableDefinitionName::OperationDefinitionName(name) => name.fmt(f),
            ExecutableDefinitionName::FragmentDefinitionName(name) => name.fmt(f),
        }
    }
}

impl From<OperationDefinitionName> for ExecutableDefinitionName {
    fn from(name: OperationDefinitionName) -> Self {
        ExecutableDefinitionName::OperationDefinitionName(name)
    }
}

impl From<FragmentDefinitionName> for ExecutableDefinitionName {
    fn from(name: FragmentDefinitionName) -> Self {
        ExecutableDefinitionName::FragmentDefinitionName(name)
    }
}

impl From<ExecutableDefinitionName> for StringKey {
    fn from(executable_definition: ExecutableDefinitionName) -> Self {
        match executable_definition {
            ExecutableDefinitionName::OperationDefinitionName(name) => name.0,
            ExecutableDefinitionName::FragmentDefinitionName(name) => name.0,
        }
    }
}

impl Lookup for ExecutableDefinitionName {
    fn lookup(self) -> &'static str {
        match self {
            ExecutableDefinitionName::OperationDefinitionName(name) => name.lookup(),
            ExecutableDefinitionName::FragmentDefinitionName(name) => name.lookup(),
        }
    }
}

impl ExecutableDefinitionName {
    pub fn unwrap_fragment_definition_name(&self) -> FragmentDefinitionName {
        match self {
            ExecutableDefinitionName::OperationDefinitionName(_) => {
                panic!("Expected FragmentDefinitionName, found {}", self)
            }
            ExecutableDefinitionName::FragmentDefinitionName(name) => *name,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct VariableName(pub StringKey);

impl Display for VariableName {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(fmt, "{}", self.0)
    }
}

impl FromStr for VariableName {
    type Err = std::convert::Infallible;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(VariableName(s.intern()))
    }
}

impl_lookup!(VariableName);

/// A variable definition of an operation or fragment
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct VariableDefinition {
    pub name: WithLocation<VariableName>,
    pub type_: TypeReference<Type>,
    pub default_value: Option<WithLocation<ConstantValue>>,
    pub directives: Vec<Directive>,
}

impl VariableDefinition {
    pub fn has_non_null_default_value(&self) -> bool {
        match &self.default_value {
            Some(value) => value.item.is_non_null(),
            _ => false,
        }
    }
}

impl Named for VariableDefinition {
    type Name = VariableName;
    fn name(&self) -> VariableName {
        self.name.item
    }
}

// Selections

/// A selection within an operation or fragment
#[derive(Clone, Eq, PartialEq)]
pub enum Selection {
    FragmentSpread(Arc<FragmentSpread>),
    InlineFragment(Arc<InlineFragment>),
    LinkedField(Arc<LinkedField>),
    ScalarField(Arc<ScalarField>),
    Condition(Arc<Condition>),
}

impl Selection {
    /// Get selection directives
    /// This method will panic if called on the Selection::Condition
    pub fn directives(&self) -> &[Directive] {
        match self {
            Selection::FragmentSpread(node) => &node.directives,
            Selection::InlineFragment(node) => &node.directives,
            Selection::ScalarField(node) => &node.directives,
            Selection::LinkedField(node) => &node.directives,
            Selection::Condition(_) => unreachable!("Unexpected `Condition` selection."),
        }
    }

    /// Update Selection directives
    /// This method will panic if called on the Selection::Condition
    pub fn set_directives(&mut self, directives: Vec<Directive>) {
        match self {
            Selection::FragmentSpread(node) => {
                Arc::make_mut(node).directives = directives;
            }
            Selection::InlineFragment(node) => {
                Arc::make_mut(node).directives = directives;
            }
            Selection::ScalarField(node) => {
                Arc::make_mut(node).directives = directives;
            }
            Selection::LinkedField(node) => {
                Arc::make_mut(node).directives = directives;
            }
            Selection::Condition(_) => unreachable!("Unexpected `Condition` selection."),
        };
    }

    /// A quick method to get the location of the selection. This may
    /// be helpful for error reporting. Please note, this implementation
    /// prefers the location of the alias for scalar and linked field selections.
    pub fn location(&self) -> Location {
        match self {
            Selection::Condition(node) => node.location,
            Selection::FragmentSpread(node) => node.fragment.location,
            Selection::InlineFragment(node) => node.spread_location,
            Selection::LinkedField(node) => node.alias_or_name_location(),
            Selection::ScalarField(node) => node.alias_or_name_location(),
        }
    }

    /// Similar to `==`, but only checking for `Arc::ptr_eq` without
    /// doing a deeper structural equality check
    pub fn ptr_eq(&self, other: &Selection) -> bool {
        match (self, other) {
            (Selection::LinkedField(a), Selection::LinkedField(b)) => Arc::ptr_eq(a, b),
            (Selection::ScalarField(a), Selection::ScalarField(b)) => Arc::ptr_eq(a, b),
            (Selection::InlineFragment(a), Selection::InlineFragment(b)) => Arc::ptr_eq(a, b),
            (Selection::FragmentSpread(a), Selection::FragmentSpread(b)) => Arc::ptr_eq(a, b),
            (Selection::Condition(a), Selection::Condition(b)) => Arc::ptr_eq(a, b),
            (Selection::LinkedField(_), _)
            | (Selection::ScalarField(_), _)
            | (Selection::InlineFragment(_), _)
            | (Selection::FragmentSpread(_), _)
            | (Selection::Condition(_), _) => false,
        }
    }

    /// Find all fragment spreads referenced the current Selection.
    /// Result deduplicated and sorted by fragment spread name
    pub fn spreaded_fragments(&self) -> Vec<Arc<FragmentSpread>> {
        let run_for_set =
            |set: &[Selection]| set.iter().flat_map(|s| s.spreaded_fragments()).collect();

        let mut all: Vec<Arc<FragmentSpread>> = match self {
            Selection::FragmentSpread(a) => vec![Arc::clone(a)],
            Selection::InlineFragment(a) => run_for_set(&a.selections),
            Selection::LinkedField(a) => run_for_set(&a.selections),
            Selection::ScalarField(_) => vec![],
            Selection::Condition(a) => run_for_set(&a.selections),
        };

        all.sort_unstable_by_key(|fs| fs.fragment.item);
        all.dedup_by_key(|fs| fs.fragment.item);
        all
    }
}

impl fmt::Debug for Selection {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Selection::FragmentSpread(node) => f.write_fmt(format_args!("{:#?}", node)),
            Selection::InlineFragment(node) => f.write_fmt(format_args!("{:#?}", node)),
            Selection::LinkedField(node) => f.write_fmt(format_args!("{:#?}", node)),
            Selection::ScalarField(node) => f.write_fmt(format_args!("{:#?}", node)),
            Selection::Condition(node) => f.write_fmt(format_args!("{:#?}", node)),
        }
    }
}

/// ... Name
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FragmentSpread {
    pub fragment: WithLocation<FragmentDefinitionName>,
    pub arguments: Vec<Argument>,
    pub directives: Vec<Directive>,
}

/// ... SelectionSet
/// ... on Type SelectionSet
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InlineFragment {
    pub type_condition: Option<Type>,
    pub directives: Vec<Directive>,
    pub selections: Vec<Selection>,
    /// Points to "..."
    pub spread_location: Location,
}
pub trait Field {
    fn alias(&self) -> Option<WithLocation<StringKey>>;
    fn definition(&self) -> WithLocation<FieldID>;
    fn arguments(&self) -> &[Argument];
    fn directives(&self) -> &[Directive];
    fn alias_or_name(&self, schema: &SDLSchema) -> StringKey {
        if let Some(name) = self.alias() {
            name.item
        } else {
            schema.field(self.definition().item).name.item
        }
    }
    fn alias_or_name_location(&self) -> Location {
        if let Some(name) = self.alias() {
            name.location
        } else {
            self.definition().location
        }
    }
}

/// Name Arguments? SelectionSet
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LinkedField {
    pub alias: Option<WithLocation<StringKey>>,
    pub definition: WithLocation<FieldID>,
    pub arguments: Vec<Argument>,
    pub directives: Vec<Directive>,
    pub selections: Vec<Selection>,
}

impl Field for LinkedField {
    fn alias(&self) -> Option<WithLocation<StringKey>> {
        self.alias
    }

    fn definition(&self) -> WithLocation<FieldID> {
        self.definition
    }

    fn arguments(&self) -> &[Argument] {
        &self.arguments
    }

    fn directives(&self) -> &[Directive] {
        &self.directives
    }
}

/// Name Arguments?
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ScalarField {
    pub alias: Option<WithLocation<StringKey>>,
    pub definition: WithLocation<FieldID>,
    pub arguments: Vec<Argument>,
    pub directives: Vec<Directive>,
}

impl Field for ScalarField {
    fn alias(&self) -> Option<WithLocation<StringKey>> {
        self.alias
    }

    fn definition(&self) -> WithLocation<FieldID> {
        self.definition
    }

    fn arguments(&self) -> &[Argument] {
        &self.arguments
    }

    fn directives(&self) -> &[Directive] {
        &self.directives
    }
}

/// https://spec.graphql.org/June2018/#sec--skip
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Condition {
    pub selections: Vec<Selection>,
    pub value: ConditionValue,
    pub passing_value: bool,
    pub location: Location,
}

impl Condition {
    pub fn directive_name(&self) -> &'static str {
        if self.passing_value {
            "include"
        } else {
            "skip"
        }
    }
}

// Associated Types

/// @ Name Arguments?
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Directive {
    pub name: WithLocation<DirectiveName>,
    pub arguments: Vec<Argument>,
    /// Optional typed data that has no textual representation. This can be used
    /// to attach arbitrary data on compiler-internal directives, such as to
    /// pass instructions to code generation.
    pub data: Option<Box<dyn AssociatedData>>,
}
impl Named for Directive {
    type Name = DirectiveName;
    fn name(&self) -> DirectiveName {
        self.name.item
    }
}

/// Name : Value
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Argument {
    pub name: WithLocation<ArgumentName>,
    pub value: WithLocation<Value>,
}
impl Named for Argument {
    type Name = ArgumentName;
    fn name(&self) -> ArgumentName {
        self.name.item
    }
}

#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum Value {
    Constant(ConstantValue),
    Variable(Variable),
    List(Vec<Value>),
    Object(Vec<Argument>),
}
impl Value {
    /// If the value is a constant, return the value, otherwise None.
    pub fn get_constant(&self) -> Option<&ConstantValue> {
        if let Value::Constant(val) = self {
            Some(val)
        } else {
            None
        }
    }

    /// If the value is a constant string literal, return the value, otherwise None.
    pub fn get_string_literal(&self) -> Option<StringKey> {
        if let Value::Constant(ConstantValue::String(val)) = self {
            Some(*val)
        } else {
            None
        }
    }

    /// Return the constant of this value.
    /// Panics if the value is not a constant.
    pub fn expect_constant(&self) -> &ConstantValue {
        self.get_constant().unwrap_or_else(|| {
            panic!("expected a constant, got {:?}", self);
        })
    }

    /// Return the constant string literal of this value.
    /// Panics if the value is not a constant string literal.
    pub fn expect_string_literal(&self) -> StringKey {
        self.get_string_literal().unwrap_or_else(|| {
            panic!("expected a string literal, got {:?}", self);
        })
    }
}

#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Variable {
    pub name: WithLocation<VariableName>,
    pub type_: TypeReference<Type>,
}

/// Name : Value[Const]
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct ConstantArgument {
    pub name: WithLocation<ArgumentName>,
    pub value: WithLocation<ConstantValue>,
}
impl Named for ConstantArgument {
    type Name = ArgumentName;
    fn name(&self) -> ArgumentName {
        self.name.item
    }
}

macro_rules! generate_unwrap_fn {
    ($fn_name:ident,$self:ident,$t:ty,$cv:pat => $result:expr) => {
        pub fn $fn_name(&$self) -> $t {
            match $self {
                $cv => $result,
                other => panic!("expected constant {} but got {:#?}", stringify!($cv), other),
            }
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum ConstantValue {
    Int(i64),
    Float(FloatValue),
    String(StringKey),
    Boolean(bool),
    Null(),
    Enum(StringKey),
    List(Vec<ConstantValue>),
    Object(Vec<ConstantArgument>),
}

impl ConstantValue {
    pub fn is_null(&self) -> bool {
        matches!(self, ConstantValue::Null())
    }

    pub fn is_non_null(&self) -> bool {
        !self.is_null()
    }

    pub fn get_string_literal(&self) -> Option<StringKey> {
        match self {
            ConstantValue::String(value) => Some(*value),
            _ => None,
        }
    }

    generate_unwrap_fn!(unwrap_int, self, i64, ConstantValue::Int(i) => *i);
    generate_unwrap_fn!(unwrap_float, self, FloatValue, ConstantValue::Float(f) => *f);
    generate_unwrap_fn!(unwrap_boolean, self, bool, ConstantValue::Boolean(b) => *b);
    generate_unwrap_fn!(unwrap_string, self, StringKey, ConstantValue::String(s) => *s);
    generate_unwrap_fn!(unwrap_enum, self, StringKey, ConstantValue::Enum(e) => *e);
    generate_unwrap_fn!(unwrap_list, self, &Vec<ConstantValue>, ConstantValue::List(l) => l);
    generate_unwrap_fn!(unwrap_object, self, &Vec<ConstantArgument>, ConstantValue::Object(o) => o);
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ConditionValue {
    Constant(bool),
    Variable(Variable),
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#![deny(warnings)]
#![deny(rust_2018_idioms)]
#![deny(clippy::all)]
#![deny(clippy::clone_on_ref_ptr)]

mod associated_data;
mod build;
mod constants;
mod errors;
mod ir;
pub mod node_identifier;
mod program;
mod signatures;
mod transform;
mod validator;
mod visitor;

pub use associated_data::AssociatedData;
pub use build::build_directive;
pub use build::build_ir as build;
pub use build::build_ir_in_relay_mode;
pub use build::build_ir_with_extra_features;
pub use build::BuilderOptions;
pub use build::FragmentVariablesSemantic;
pub use build::RelayMode;
pub use build::DIRECTIVE_ARGUMENTS;
pub use build::FIXME_FAT_INTERFACE;
pub use constants::ARGUMENT_DEFINITION;
pub use ir::*;
pub use program::Program;
pub use signatures::ProvidedVariableMetadata;
pub use signatures::UNUSED_LOCAL_VARIABLE_DEPRECATED;
pub use transform::transform_list;
pub use transform::transform_list_multi;
pub use transform::Transformed;
pub use transform::TransformedMulti;
pub use transform::TransformedValue;
pub use transform::Transformer;
pub use validator::Validator;
pub use visitor::Visitor;

pub use crate::errors::ValidationMessage;

/// Re-exported values to be used by the `associated_data_impl!` macro.
pub mod reexport {
    pub use common::NamedItem;
    pub use common::WithLocation;
    pub use fnv::FnvHasher;
    pub use intern::string_key;
    pub use intern::string_key::Intern;
    pub use intern::string_key::StringKey;
    pub use once_cell::sync::Lazy;

    pub use crate::associated_data::AsAny;
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::fmt;
use std::hash::Hash;
use std::hash::Hasher;
use std::marker::PhantomData;
use std::sync::Arc;

use common::ArgumentName;
use common::DirectiveName;
use common::WithLocation;
use graphql_syntax::OperationKind;
use intern::string_key::StringKey;
use schema::FieldID;
use schema::SDLSchema;
use schema::Type;
use schema::TypeReference;

use crate::*;

#[derive(Clone, Debug)]
enum NodeIdentifierInner<TBehavior: LocationAgnosticBehavior> {
    LinkedField(LinkedFieldIdentifier<TBehavior>),
    ScalarField(ScalarFieldIdentifier<TBehavior>),
    FragmentSpread(Arc<FragmentSpread>),
    InlineFragment(Arc<InlineFragment>),
    Condition(Arc<Condition>),
}

/// An identifier that is unique to a given selection: the alias for
/// fields, the type for inline fragments, and a summary of the condition
/// variable and passing value for conditions.
#[derive(Clone)]
pub struct NodeIdentifier<TBehavior: LocationAgnosticBehavior> {
    inner: NodeIdentifierInner<TBehavior>,
    _behavior: PhantomData<TBehavior>,
}

impl<TBehavior: LocationAgnosticBehavior> NodeIdentifier<TBehavior> {
    pub fn from_selection(schema: &SDLSchema, selection: &Selection, _: TBehavior) -> Self {
        let inner = match selection {
            Selection::LinkedField(node) => {
                NodeIdentifierInner::LinkedField(LinkedFieldIdentifier {
                    alias_or_name: node.alias_or_name(schema),
                    node: Arc::clone(node),
                    _behavior: PhantomData::<TBehavior>,
                })
            }
            Selection::ScalarField(node) => {
                NodeIdentifierInner::ScalarField(ScalarFieldIdentifier {
                    alias_or_name: node.alias_or_name(schema),
                    node: Arc::clone(node),
                    _behavior: PhantomData::<TBehavior>,
                })
            }
            Selection::InlineFragment(node) => {
                NodeIdentifierInner::InlineFragment(Arc::clone(node))
            }
            Selection::FragmentSpread(node) => {
                NodeIdentifierInner::FragmentSpread(Arc::clone(node))
            }
            Selection::Condition(node) => NodeIdentifierInner::Condition(Arc::clone(node)),
        };

        Self {
            inner,
            _behavior: PhantomData::<TBehavior>,
        }
    }

    pub fn are_equal(schema: &SDLSchema, a: &Selection, b: &Selection) -> bool {
        match (a, b) {
            (Selection::FragmentSpread(a), Selection::FragmentSpread(b)) => {
                a.fragment
                    .item
                    .location_agnostic_eq::<TBehavior>(&b.fragment.item)
                    && a.arguments.location_agnostic_eq::<TBehavior>(&b.arguments)
                    && a.directives
                        .location_agnostic_eq::<TBehavior>(&b.directives)
            }
            (Selection::InlineFragment(a), Selection::InlineFragment(b)) => {
                a.type_condition
                    .location_agnostic_eq::<TBehavior>(&b.type_condition)
                    && a.directives
                        .location_agnostic_eq::<TBehavior>(&b.directives)
            }
            (Selection::LinkedField(a), Selection::LinkedField(b)) => {
                a.alias_or_name(schema)
                    .location_agnostic_eq::<TBehavior>(&b.alias_or_name(schema))
                    && a.directives
                        .location_agnostic_eq::<TBehavior>(&b.directives)
            }
            (Selection::ScalarField(a), Selection::ScalarField(b)) => {
                a.alias_or_name(schema)
                    .location_agnostic_eq::<TBehavior>(&b.alias_or_name(schema))
                    && a.directives
                        .location_agnostic_eq::<TBehavior>(&b.directives)
            }
            (Selection::Condition(a), Selection::Condition(b)) => {
                a.passing_value
                    .location_agnostic_eq::<TBehavior>(&b.passing_value)
                    && a.value.location_agnostic_eq::<TBehavior>(&b.value)
            }

            (Selection::FragmentSpread(_), _) => false,
            (Selection::InlineFragment(_), _) => false,
            (Selection::LinkedField(_), _) => false,
            (Selection::ScalarField(_), _) => false,
            (Selection::Condition(_), _) => false,
        }
    }
}

impl<TBehavior: LocationAgnosticBehavior> PartialEq for NodeIdentifier<TBehavior> {
    fn eq(&self, other: &Self) -> bool {
        match (&self.inner, &other.inner) {
            (NodeIdentifierInner::LinkedField(l), NodeIdentifierInner::LinkedField(r)) => l.eq(r),
            (NodeIdentifierInner::ScalarField(l), NodeIdentifierInner::ScalarField(r)) => l.eq(r),
            (NodeIdentifierInner::FragmentSpread(l), NodeIdentifierInner::FragmentSpread(r)) => {
                l.fragment
                    .item
                    .location_agnostic_eq::<TBehavior>(&r.fragment.item)
                    && l.arguments.location_agnostic_eq::<TBehavior>(&r.arguments)
                    && l.directives
                        .location_agnostic_eq::<TBehavior>(&r.directives)
            }
            (NodeIdentifierInner::InlineFragment(l), NodeIdentifierInner::InlineFragment(r)) => {
                l.type_condition
                    .location_agnostic_eq::<TBehavior>(&r.type_condition)
                    && l.directives
                        .location_agnostic_eq::<TBehavior>(&r.directives)
            }
            (NodeIdentifierInner::Condition(l), NodeIdentifierInner::Condition(r)) => {
                l.passing_value
                    .location_agnostic_eq::<TBehavior>(&r.passing_value)
                    && l.value.location_agnostic_eq::<TBehavior>(&r.value)
            }
            (NodeIdentifierInner::LinkedField(_), _) => false,
            (NodeIdentifierInner::ScalarField(_), _) => false,
            (NodeIdentifierInner::FragmentSpread(_), _) => false,
            (NodeIdentifierInner::InlineFragment(_), _) => false,
            (NodeIdentifierInner::Condition(_), _) => false,
        }
    }
}

impl<TBehavior: LocationAgnosticBehavior> Eq for NodeIdentifier<TBehavior> {}

impl<TBehavior: LocationAgnosticBehavior> Hash for NodeIdentifier<TBehavior> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        match &self.inner {
            NodeIdentifierInner::LinkedField(v) => v.hash(state),
            NodeIdentifierInner::ScalarField(v) => v.hash(state),
            NodeIdentifierInner::FragmentSpread(v) => {
                v.fragment.item.hash(state);
                v.arguments.location_agnostic_hash::<_, TBehavior>(state);
                v.directives.location_agnostic_hash::<_, TBehavior>(state);
            }
            NodeIdentifierInner::InlineFragment(v) => {
                v.type_condition.hash(state);
                v.directives.location_agnostic_hash::<_, TBehavior>(state);
            }
            NodeIdentifierInner::Condition(v) => {
                v.passing_value.hash(state);
                v.value.location_agnostic_hash::<_, TBehavior>(state);
            }
        }
    }
}

impl<TBehavior: LocationAgnosticBehavior> fmt::Debug for NodeIdentifier<TBehavior> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.inner {
            NodeIdentifierInner::LinkedField(v) => {
                write!(f, "NodeIdentifierInner::LinkedField({:?})", v)
            }
            NodeIdentifierInner::ScalarField(v) => {
                write!(f, "NodeIdentifierInner::ScalarField({:?})", v)
            }
            NodeIdentifierInner::FragmentSpread(v) => {
                write!(f, "NodeIdentifierInner::FragmentSpread({:?})", v)
            }
            NodeIdentifierInner::InlineFragment(v) => {
                write!(f, "NodeIdentifierInner::InlineFragment({:?})", v)
            }
            NodeIdentifierInner::Condition(v) => {
                write!(f, "NodeIdentifierInner::Condition({:?})", v)
            }
        }
    }
}

// Implement ScalarFieldIdentifier and LinkedFieldIdentifier separately for computing alias name
#[derive(Clone)]
pub struct ScalarFieldIdentifier<TBehavior: LocationAgnosticBehavior> {
    alias_or_name: StringKey,
    node: Arc<ScalarField>,
    _behavior: PhantomData<TBehavior>,
}
impl<TBehavior: LocationAgnosticBehavior> PartialEq for ScalarFieldIdentifier<TBehavior> {
    fn eq(&self, other: &Self) -> bool {
        self.alias_or_name
            .location_agnostic_eq::<TBehavior>(&other.alias_or_name)
            && self
                .node
                .directives
                .location_agnostic_eq::<TBehavior>(&other.node.directives)
    }
}
impl<TBehavior: LocationAgnosticBehavior> Hash for ScalarFieldIdentifier<TBehavior> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.alias_or_name.hash(state);
        self.node
            .directives
            .location_agnostic_hash::<_, TBehavior>(state);
    }
}
impl<TBehavior: LocationAgnosticBehavior> fmt::Debug for ScalarFieldIdentifier<TBehavior> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ScalarFieldIdentifier")
            .field("alias_or_name", &self.alias_or_name)
            .field("node", &self.node)
            .finish()
    }
}
impl<TBehavior: LocationAgnosticBehavior> Eq for ScalarFieldIdentifier<TBehavior> {}

#[derive(Clone)]
pub struct LinkedFieldIdentifier<TBehavior: LocationAgnosticBehavior> {
    alias_or_name: StringKey,
    node: Arc<LinkedField>,
    _behavior: PhantomData<TBehavior>,
}
impl<TBehavior: LocationAgnosticBehavior> PartialEq for LinkedFieldIdentifier<TBehavior> {
    fn eq(&self, other: &Self) -> bool {
        self.alias_or_name
            .location_agnostic_eq::<TBehavior>(&other.alias_or_name)
            && self
                .node
                .directives
                .location_agnostic_eq::<TBehavior>(&other.node.directives)
    }
}
impl<TBehavior: LocationAgnosticBehavior> Hash for LinkedFieldIdentifier<TBehavior> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.alias_or_name.hash(state);
        self.node
            .directives
            .location_agnostic_hash::<_, TBehavior>(state);
    }
}
impl<TBehavior: LocationAgnosticBehavior> fmt::Debug for LinkedFieldIdentifier<TBehavior> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("LinkedFieldIdentifier")
            .field("alias_or_name", &self.alias_or_name)
            .field("node", &self.node)
            .finish()
    }
}
impl<TBehavior: LocationAgnosticBehavior> Eq for LinkedFieldIdentifier<TBehavior> {}

/// Below are the implmentations of LocationAgnosticPartialEq and LocationAgnosticHash
/// for `Directive` and `Argument`, so that we can check equality and hash node identities
/// ignoring the location.
pub trait LocationAgnosticPartialEq {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool;
}

pub trait LocationAgnosticHash {
    fn location_agnostic_hash<H: Hasher, B: LocationAgnosticBehavior>(&self, _state: &mut H);
}

/// Additional behaviors that appliable to the implementation of `LocationAgnosticPartialEq`
/// and `LocationAgnosticHash`
pub trait LocationAgnosticBehavior {
    fn should_skip_in_node_identifier(name: DirectiveName) -> bool;
    fn hash_for_name_only(name: DirectiveName) -> bool;
}

impl<T: LocationAgnosticHash> LocationAgnosticHash for WithLocation<T> {
    fn location_agnostic_hash<H: (Hasher), B: LocationAgnosticBehavior>(&self, state: &mut H) {
        self.item.location_agnostic_hash::<_, B>(state);
    }
}

impl<T: LocationAgnosticPartialEq> LocationAgnosticPartialEq for WithLocation<T> {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.item.location_agnostic_eq::<B>(&other.item)
    }
}

impl<T: LocationAgnosticPartialEq> LocationAgnosticPartialEq for Option<T> {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        match (self, other) {
            (Some(l), Some(r)) => l.location_agnostic_eq::<B>(r),
            (None, None) => true,
            _ => false,
        }
    }
}

impl<T: LocationAgnosticPartialEq> LocationAgnosticPartialEq for &T {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        (*self).location_agnostic_eq::<B>(*other)
    }
}

impl LocationAgnosticHash for StringKey {
    fn location_agnostic_hash<H: Hasher, B: LocationAgnosticBehavior>(&self, state: &mut H) {
        self.hash(state);
    }
}

// Limit the generic to enable a different implementation for Vec<Directive> and Vec<Argument>
pub trait DirectlyComparableIR {}
impl DirectlyComparableIR for Value {}
impl DirectlyComparableIR for ConstantArgument {}
impl DirectlyComparableIR for ConstantValue {}
impl DirectlyComparableIR for Selection {}
impl DirectlyComparableIR for ExecutableDefinition {}
impl DirectlyComparableIR for VariableDefinition {}

impl<T: LocationAgnosticPartialEq + DirectlyComparableIR> LocationAgnosticPartialEq for Vec<T> {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.len() == other.len()
            && self
                .iter()
                .zip(other.iter())
                .all(|(left, right)| left.location_agnostic_eq::<B>(right))
    }
}

impl<T: LocationAgnosticHash> LocationAgnosticHash for Vec<T> {
    fn location_agnostic_hash<H: Hasher, B: LocationAgnosticBehavior>(&self, state: &mut H) {
        for v in self {
            v.location_agnostic_hash::<_, B>(state);
        }
    }
}

fn filtered_location_agnostic_eq<
    T: LocationAgnosticPartialEq,
    F: Copy + for<'r> FnMut(&'r &T) -> bool,
    B: LocationAgnosticBehavior,
>(
    left: &[T],
    right: &[T],
    filter: F,
) -> bool {
    let mut left = left.iter().filter(filter);
    let mut right = right.iter().filter(filter);
    loop {
        let x = match left.next() {
            None => return right.next().is_none(),
            Some(val) => val,
        };

        let y = match right.next() {
            None => return false,
            Some(val) => val,
        };

        if !x.location_agnostic_eq::<B>(y) {
            return false;
        }
    }
}

impl LocationAgnosticPartialEq for Vec<Directive> {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        filtered_location_agnostic_eq::<_, _, B>(self, other, |d| {
            !B::should_skip_in_node_identifier(d.name.item)
        })
    }
}

impl LocationAgnosticHash for Directive {
    fn location_agnostic_hash<H: Hasher, B: LocationAgnosticBehavior>(&self, state: &mut H) {
        if B::hash_for_name_only(self.name.item) {
            self.name.hash(state);
        } else if !B::should_skip_in_node_identifier(self.name.item) {
            self.name.item.0.location_agnostic_hash::<_, B>(state);
            self.arguments.location_agnostic_hash::<_, B>(state);
            self.data.hash(state);
        }
    }
}

impl LocationAgnosticPartialEq for Directive {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        if !self.name.item.location_agnostic_eq::<B>(&other.name.item) {
            return false;
        }
        if B::hash_for_name_only(self.name.item) {
            return true;
        }
        self.arguments.location_agnostic_eq::<B>(&other.arguments) && self.data == other.data
    }
}

impl LocationAgnosticPartialEq for Vec<Argument> {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        filtered_location_agnostic_eq::<_, _, B>(self, other, |a| {
            !matches!(a.value.item, Value::Constant(ConstantValue::Null()))
        })
    }
}

impl LocationAgnosticHash for Argument {
    fn location_agnostic_hash<H: Hasher, B: LocationAgnosticBehavior>(&self, state: &mut H) {
        if !matches!(self.value.item, Value::Constant(ConstantValue::Null())) {
            self.name.item.0.location_agnostic_hash::<_, B>(state);
            self.value.location_agnostic_hash::<_, B>(state);
        }
    }
}

impl LocationAgnosticPartialEq for Argument {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.name.item.location_agnostic_eq::<B>(&other.name.item)
            && self.value.location_agnostic_eq::<B>(&other.value)
    }
}

impl LocationAgnosticHash for Value {
    fn location_agnostic_hash<H: Hasher, B: LocationAgnosticBehavior>(&self, state: &mut H) {
        match self {
            Value::Constant(v) => v.location_agnostic_hash::<_, B>(state),
            Value::Variable(v) => v.location_agnostic_hash::<_, B>(state),
            Value::List(v) => v.location_agnostic_hash::<_, B>(state),
            Value::Object(v) => v.location_agnostic_hash::<_, B>(state),
        }
    }
}

impl LocationAgnosticPartialEq for Value {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        match (self, other) {
            (Value::Constant(l), Value::Constant(r)) => l.location_agnostic_eq::<B>(r),
            (Value::Variable(l), Value::Variable(r)) => l.location_agnostic_eq::<B>(r),
            (Value::List(l), Value::List(r)) => l.location_agnostic_eq::<B>(r),
            (Value::Object(l), Value::Object(r)) => l.location_agnostic_eq::<B>(r),
            (Value::Constant(_), _) => false,
            (Value::Variable(_), _) => false,
            (Value::List(_), _) => false,
            (Value::Object(_), _) => false,
        }
    }
}

impl LocationAgnosticHash for Variable {
    fn location_agnostic_hash<H: Hasher, B: LocationAgnosticBehavior>(&self, state: &mut H) {
        self.name.item.0.location_agnostic_hash::<_, B>(state);
        self.type_.hash(state);
    }
}

impl LocationAgnosticPartialEq for Variable {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.name.item.location_agnostic_eq::<B>(&other.name.item) && self.type_.eq(&other.type_)
    }
}

impl LocationAgnosticHash for ConstantArgument {
    fn location_agnostic_hash<H: Hasher, B: LocationAgnosticBehavior>(&self, state: &mut H) {
        self.name.item.0.location_agnostic_hash::<_, B>(state);
        self.value.location_agnostic_hash::<_, B>(state);
    }
}

impl LocationAgnosticPartialEq for ConstantArgument {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.name.item.location_agnostic_eq::<B>(&other.name.item)
            && self.value.location_agnostic_eq::<B>(&other.value)
    }
}

impl LocationAgnosticHash for ConstantValue {
    fn location_agnostic_hash<H: Hasher, B: LocationAgnosticBehavior>(&self, state: &mut H) {
        match self {
            ConstantValue::Int(v) => v.hash(state),
            ConstantValue::Float(v) => v.hash(state),
            ConstantValue::String(_) => self.hash(state),
            ConstantValue::Boolean(v) => v.hash(state),
            ConstantValue::Null() => self.hash(state),
            ConstantValue::Enum(_) => self.hash(state),
            ConstantValue::List(v) => v.location_agnostic_hash::<_, B>(state),
            ConstantValue::Object(v) => v.location_agnostic_hash::<_, B>(state),
        }
    }
}

impl LocationAgnosticPartialEq for ConstantValue {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        match (self, other) {
            (ConstantValue::Int(l), ConstantValue::Int(r)) => l.eq(r),
            (ConstantValue::Float(l), ConstantValue::Float(r)) => l.eq(r),
            (ConstantValue::String(l), ConstantValue::String(r)) => l.eq(r),
            (ConstantValue::Boolean(l), ConstantValue::Boolean(r)) => l.eq(r),
            (ConstantValue::Null(), ConstantValue::Null()) => true,
            (ConstantValue::Enum(l), ConstantValue::Enum(r)) => l.eq(r),
            (ConstantValue::List(l), ConstantValue::List(r)) => l.location_agnostic_eq::<B>(r),
            (ConstantValue::Object(l), ConstantValue::Object(r)) => l.location_agnostic_eq::<B>(r),
            (ConstantValue::Int(_), _) => false,
            (ConstantValue::Float(_), _) => false,
            (ConstantValue::String(_), _) => false,
            (ConstantValue::Boolean(_), _) => false,
            (ConstantValue::Null(), _) => false,
            (ConstantValue::Enum(_), _) => false,
            (ConstantValue::List(_), _) => false,
            (ConstantValue::Object(_), _) => false,
        }
    }
}

impl LocationAgnosticHash for ConditionValue {
    fn location_agnostic_hash<H: Hasher, B: LocationAgnosticBehavior>(&self, state: &mut H) {
        match self {
            ConditionValue::Constant(constant) => constant.hash(state),
            ConditionValue::Variable(variable) => variable.name.item.hash(state),
        }
    }
}

impl LocationAgnosticPartialEq for ConditionValue {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        match (self, other) {
            (ConditionValue::Constant(left), ConditionValue::Constant(right)) => {
                left.location_agnostic_eq::<B>(right)
            }
            (ConditionValue::Variable(left), ConditionValue::Variable(right)) => {
                left.location_agnostic_eq::<B>(right)
            }
            (ConditionValue::Constant(_), _) => false,
            (ConditionValue::Variable(_), _) => false,
        }
    }
}

impl LocationAgnosticPartialEq for ExecutableDefinition {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        match (self, other) {
            (ExecutableDefinition::Operation(left), ExecutableDefinition::Operation(right)) => {
                left.location_agnostic_eq::<B>(right)
            }
            (ExecutableDefinition::Fragment(left), ExecutableDefinition::Fragment(right)) => {
                left.location_agnostic_eq::<B>(right)
            }
            _ => false,
        }
    }
}

impl LocationAgnosticPartialEq for VariableDefinition {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.name.location_agnostic_eq::<B>(&other.name)
            && self.type_.location_agnostic_eq::<B>(&other.type_)
            && self
                .default_value
                .location_agnostic_eq::<B>(&other.default_value)
            && self.directives.location_agnostic_eq::<B>(&other.directives)
    }
}

impl LocationAgnosticPartialEq for OperationDefinition {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.kind.location_agnostic_eq::<B>(&other.kind)
            && self.name.location_agnostic_eq::<B>(&other.name)
            && self.type_.location_agnostic_eq::<B>(&other.type_)
            && self
                .variable_definitions
                .location_agnostic_eq::<B>(&other.variable_definitions)
            && self.directives.location_agnostic_eq::<B>(&other.directives)
            && self.selections.location_agnostic_eq::<B>(&other.selections)
    }
}

macro_rules! impl_location_agnostic_partial_eq_using_eq {
    ($type:ident) => {
        impl LocationAgnosticPartialEq for $type {
            fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
                self == other
            }
        }
    };
}
impl_location_agnostic_partial_eq_using_eq!(Type);
impl_location_agnostic_partial_eq_using_eq!(OperationKind);
impl_location_agnostic_partial_eq_using_eq!(FieldID);
impl_location_agnostic_partial_eq_using_eq!(OperationDefinitionName);
impl_location_agnostic_partial_eq_using_eq!(FragmentDefinitionName);
impl_location_agnostic_partial_eq_using_eq!(VariableName);
impl_location_agnostic_partial_eq_using_eq!(DirectiveName);
impl_location_agnostic_partial_eq_using_eq!(ArgumentName);
impl_location_agnostic_partial_eq_using_eq!(bool);
impl_location_agnostic_partial_eq_using_eq!(StringKey);

impl<T: PartialEq> LocationAgnosticPartialEq for TypeReference<T> {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        *self == *other
    }
}

impl LocationAgnosticPartialEq for Selection {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        match (self, other) {
            (Selection::FragmentSpread(l), Selection::FragmentSpread(r)) => {
                l.location_agnostic_eq::<B>(r)
            }
            (Selection::InlineFragment(l), Selection::InlineFragment(r)) => {
                l.location_agnostic_eq::<B>(r)
            }
            (Selection::LinkedField(l), Selection::LinkedField(r)) => {
                l.location_agnostic_eq::<B>(r)
            }
            (Selection::ScalarField(l), Selection::ScalarField(r)) => {
                l.location_agnostic_eq::<B>(r)
            }
            (Selection::Condition(l), Selection::Condition(r)) => l.location_agnostic_eq::<B>(r),
            _ => false,
        }
    }
}

impl LocationAgnosticPartialEq for FragmentSpread {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.fragment.location_agnostic_eq::<B>(&other.fragment)
            && self.arguments.location_agnostic_eq::<B>(&other.arguments)
            && self.directives.location_agnostic_eq::<B>(&other.directives)
    }
}

impl LocationAgnosticPartialEq for InlineFragment {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.type_condition
            .location_agnostic_eq::<B>(&other.type_condition)
            && self.directives.location_agnostic_eq::<B>(&other.directives)
            && self.selections.location_agnostic_eq::<B>(&other.selections)
    }
}

impl LocationAgnosticPartialEq for LinkedField {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.alias.location_agnostic_eq::<B>(&other.alias)
            && self.definition.location_agnostic_eq::<B>(&other.definition)
            && self.arguments.location_agnostic_eq::<B>(&other.arguments)
            && self.directives.location_agnostic_eq::<B>(&other.directives)
            && self.selections.location_agnostic_eq::<B>(&other.selections)
    }
}

impl LocationAgnosticPartialEq for ScalarField {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.alias.location_agnostic_eq::<B>(&other.alias)
            && self.definition.location_agnostic_eq::<B>(&other.definition)
            && self.arguments.location_agnostic_eq::<B>(&other.arguments)
            && self.directives.location_agnostic_eq::<B>(&other.directives)
    }
}

impl LocationAgnosticPartialEq for Condition {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.selections.location_agnostic_eq::<B>(&other.selections)
            && self.value.location_agnostic_eq::<B>(&other.value)
            && self
                .passing_value
                .location_agnostic_eq::<B>(&other.passing_value)
    }
}

impl LocationAgnosticPartialEq for FragmentDefinition {
    fn location_agnostic_eq<B: LocationAgnosticBehavior>(&self, other: &Self) -> bool {
        self.name.location_agnostic_eq::<B>(&other.name)
            && self
                .variable_definitions
                .location_agnostic_eq::<B>(&other.variable_definitions)
            && self
                .used_global_variables
                .location_agnostic_eq::<B>(&other.used_global_variables)
            && self
                .type_condition
                .location_agnostic_eq::<B>(&other.type_condition)
            && self.directives.location_agnostic_eq::<B>(&other.directives)
            && self.selections.location_agnostic_eq::<B>(&other.selections)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::collections::HashMap;
use std::sync::Arc;

use intern::BuildIdHasher;
use schema::SDLSchema;

use crate::ir::ExecutableDefinition;
use crate::ir::FragmentDefinition;
use crate::ir::FragmentDefinitionName;
use crate::ir::FragmentDefinitionNameMap;
use crate::ir::OperationDefinition;
use crate::ir::OperationDefinitionName;
use crate::ExecutableDefinitionName;

/// A collection of all documents that are being compiled.
#[derive(Debug, Clone)]
pub struct Program {
    pub schema: Arc<SDLSchema>,
    pub fragments: FragmentDefinitionNameMap<Arc<FragmentDefinition>>,
    pub operations: Vec<Arc<OperationDefinition>>,
}

impl Program {
    pub fn new(schema: Arc<SDLSchema>) -> Self {
        Self {
            schema,
            fragments: Default::default(),
            operations: Default::default(),
        }
    }

    pub fn from_definitions(
        schema: Arc<SDLSchema>,
        definitions: Vec<ExecutableDefinition>,
    ) -> Self {
        let mut operations = Vec::new();
        let mut fragments = HashMap::default();
        let mut seen_operation_loc = HashMap::new();
        for definition in definitions {
            match definition {
                ExecutableDefinition::Operation(operation) => {
                    let loc = operation.name.location;
                    let name = operation.name.item;
                    if let Some(another) = seen_operation_loc.insert(name, loc) {
                        panic!(
                            "\nDuplicate operation definitions named {}: \nfirst one: {:?}\nsecond one: {:?}\n",
                            name, loc, another
                        );
                    }
                    operations.push(Arc::new(operation)); // Keep the order the operations same as inputs.
                }
                ExecutableDefinition::Fragment(fragment) => {
                    let loc = fragment.name.location;
                    let name = fragment.name.item;
                    if let Some(another) = fragments.insert(name, Arc::new(fragment)) {
                        panic!(
                            "\nDuplicate fragment definitions named {}: \nfirst one: {:?}\nsecond one: {:?}\n",
                            name, loc, &another.name.location
                        );
                    }
                }
            }
        }
        Self {
            schema,
            fragments,
            operations,
        }
    }

    pub fn insert_fragment(&mut self, fragment: Arc<FragmentDefinition>) {
        let name = fragment.name.item;
        if let Some(previous) = self.fragments.insert(name, fragment) {
            panic!(
                "Can only insert '{}' once. Had {:?} and trying to insert {:?}.",
                name, previous, self.fragments[&name]
            );
        };
    }

    pub fn fragment(&self, name: FragmentDefinitionName) -> Option<&Arc<FragmentDefinition>> {
        self.fragments.get(&name)
    }

    pub fn fragment_mut(
        &mut self,
        name: FragmentDefinitionName,
    ) -> Option<&mut Arc<FragmentDefinition>> {
        self.fragments.get_mut(&name)
    }

    /// Searches for an operation by name.
    ///
    /// NOTE: This is a linear search, we currently don't frequently search
    ///       for operations by name, so this might be overall faster than
    ///       using a map internally.
    pub fn operation(&self, name: OperationDefinitionName) -> Option<&Arc<OperationDefinition>> {
        self.operations()
            .find(|operation| operation.name.item == name)
    }

    pub fn insert_operation(&mut self, operation: Arc<OperationDefinition>) {
        self.operations.push(operation);
    }

    pub fn operations(&self) -> impl Iterator<Item = &Arc<OperationDefinition>> {
        self.operations.iter()
    }

    pub fn fragments(&self) -> impl Iterator<Item = &Arc<FragmentDefinition>> {
        self.fragments.values()
    }

    pub fn document_count(&self) -> usize {
        self.fragments.len() + self.operations.len()
    }

    pub fn merge_program(
        &mut self,
        other_program: &Self,
        removed_definition_names: Option<&[ExecutableDefinitionName]>,
    ) {
        let mut operations: HashMap<
            OperationDefinitionName,
            Arc<OperationDefinition>,
            BuildIdHasher<u32>,
        > = self
            .operations
            .drain(..)
            .map(|op| (op.name.item, op))
            .collect();
        for fragment in other_program.fragments() {
            self.fragments
                .insert(fragment.name.item, Arc::clone(fragment));
        }
        for operation in other_program.operations() {
            operations.insert(operation.name.item, Arc::clone(operation));
        }
        if let Some(removed_definition_names) = removed_definition_names {
            for removed in removed_definition_names {
                match removed {
                    ExecutableDefinitionName::OperationDefinitionName(name) => {
                        operations.remove(name);
                    }
                    ExecutableDefinitionName::FragmentDefinitionName(name) => {
                        self.fragments.remove(name);
                    }
                };
            }
        }
        self.operations
            .extend(operations.into_iter().map(|(_, op)| op));
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::collections::HashMap;
use std::path::PathBuf;

use common::ArgumentName;
use common::Diagnostic;
use common::DiagnosticsResult;
use common::DirectiveName;
use common::Location;
use common::SourceLocationKey;
use common::WithLocation;
use errors::par_try_map;
use errors::try2;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use intern::Lookup;
use lazy_static::lazy_static;
use schema::suggestion_list::GraphQLSuggestions;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;
use schema::TypeReference;

use crate::associated_data_impl;
use crate::build::build_constant_value;
use crate::build::build_type_annotation;
use crate::build::build_variable_definitions;
use crate::build::ValidationLevel;
use crate::build_directive;
use crate::constants::ARGUMENT_DEFINITION;
use crate::errors::ValidationMessage;
use crate::errors::ValidationMessageWithData;
use crate::ir::ConstantValue;
use crate::ir::FragmentDefinitionName;
use crate::ir::FragmentDefinitionNameMap;
use crate::ir::VariableDefinition;
use crate::VariableName;

lazy_static! {
    static ref TYPE: StringKey = "type".intern();
    static ref DEFAULT_VALUE: StringKey = "defaultValue".intern();
    static ref PROVIDER: StringKey = "provider".intern();
    pub static ref UNUSED_LOCAL_VARIABLE_DEPRECATED: DirectiveName =
        DirectiveName("unusedLocalVariable_DEPRECATED".intern());
    static ref DIRECTIVES: StringKey = "directives".intern();
}

pub type FragmentSignatures = FragmentDefinitionNameMap<FragmentSignature>;

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ProvidedVariableMetadata {
    pub module_name: StringKey,
    pub original_variable_name: VariableName,
    fragment_source_location: SourceLocationKey,
}

impl ProvidedVariableMetadata {
    /// Return a path to the provider module, based on the fragment location
    /// where this provider is used.
    pub fn module_path(&self) -> PathBuf {
        let mut fragment_path = PathBuf::from(self.fragment_source_location.path());
        fragment_path.pop();
        fragment_path.push(PathBuf::from(self.module_name.lookup()));

        fragment_path
    }
}

associated_data_impl!(ProvidedVariableMetadata);

/// Describes the public API of a fragment, excluding its selections.
/// When translating ASTs to IR, fragment spread arguments must be
/// checked against the corresponding fragment definition. However,
/// this process can't use the IR fragment definition, since that
/// would depend on having checked its body! Since recursive fragments
/// are allowed, we break the cycle by first computing signatures
/// and using these to type check fragment spreads in selections.
#[derive(Debug, Eq, PartialEq)]
pub struct FragmentSignature {
    pub name: WithLocation<FragmentDefinitionName>,
    pub variable_definitions: Vec<VariableDefinition>,
    pub type_condition: Type,
}

pub fn build_signatures(
    schema: &SDLSchema,
    definitions: &[graphql_syntax::ExecutableDefinition],
) -> DiagnosticsResult<FragmentSignatures> {
    let suggestions = GraphQLSuggestions::new(schema);
    let mut seen_signatures: FragmentDefinitionNameMap<FragmentSignature> =
        HashMap::with_capacity_and_hasher(definitions.len(), Default::default());
    let signatures = par_try_map(definitions, |definition| match definition {
        graphql_syntax::ExecutableDefinition::Fragment(fragment) => Ok(Some(
            build_fragment_signature(schema, fragment, &suggestions)?,
        )),
        graphql_syntax::ExecutableDefinition::Operation(_) => Ok(None),
    })?;
    let mut errors = Vec::new();
    for signature in signatures.into_iter().flatten() {
        let previous_signature = seen_signatures.get(&signature.name.item);
        if let Some(previous_signature) = previous_signature {
            errors.push(
                Diagnostic::error(
                    ValidationMessage::DuplicateDefinition(signature.name.item.0),
                    previous_signature.name.location,
                )
                .annotate("also defined here", signature.name.location),
            );
            continue;
        }
        seen_signatures.insert(signature.name.item, signature);
    }
    if errors.is_empty() {
        Ok(seen_signatures)
    } else {
        Err(errors)
    }
}

fn build_fragment_signature(
    schema: &SDLSchema,
    fragment: &graphql_syntax::FragmentDefinition,
    suggestions: &GraphQLSuggestions<'_>,
) -> DiagnosticsResult<FragmentSignature> {
    let type_name = fragment.type_condition.type_.value;
    let type_condition = match schema.get_type(type_name) {
        Some(type_condition) => match type_condition {
            Type::Interface(..) | Type::Object(..) | Type::Union(..) => Ok(type_condition),
            _ => Err(Diagnostic::error(
                ValidationMessage::ExpectedCompositeType(type_condition),
                fragment
                    .location
                    .with_span(fragment.type_condition.type_.span),
            )
            .into()),
        },
        None => Err(Diagnostic::error_with_data(
            ValidationMessageWithData::UnknownType {
                type_name,
                suggestions: suggestions.composite_type_suggestions(type_name),
            },
            fragment
                .location
                .with_span(fragment.type_condition.type_.span),
        )
        .into()),
    };
    let argument_definition_directives = fragment
        .directives
        .iter()
        .filter(|x| DirectiveName(x.name.value) == *ARGUMENT_DEFINITION)
        .collect::<Vec<_>>();
    if fragment.variable_definitions.is_some() && !argument_definition_directives.is_empty() {
        return Err(Diagnostic::error(
            ValidationMessage::VariableDefinitionsAndArgumentDirective,
            fragment
                .location
                .with_span(argument_definition_directives[0].span),
        )
        .annotate(
            "variables are previously defined here",
            fragment.location.with_span(
                fragment
                    .variable_definitions
                    .as_ref()
                    .map(|list| list.span)
                    .unwrap(),
            ),
        )
        .into());
    } else if argument_definition_directives.len() > 1 {
        return Err(Diagnostic::error(
            ValidationMessage::ExpectedOneArgumentDefinitionsDirective,
            fragment
                .location
                .with_span(argument_definition_directives[1].span),
        )
        .into());
    }
    let variable_definitions = fragment
        .variable_definitions
        .as_ref()
        .map(|variable_definitions| {
            build_variable_definitions(schema, &variable_definitions.items, fragment.location)
        })
        .or_else(|| {
            argument_definition_directives
                .get(0)
                .map(|x| build_fragment_variable_definitions(schema, fragment, x))
        })
        .unwrap_or_else(|| Ok(Default::default()));

    let (type_condition, variable_definitions) = try2(type_condition, variable_definitions)?;

    Ok(FragmentSignature {
        name: WithLocation::from_span(
            fragment.location.source_location(),
            fragment.name.span,
            FragmentDefinitionName(fragment.name.value),
        ),
        type_condition,
        variable_definitions,
    })
}

fn build_fragment_variable_definitions(
    schema: &SDLSchema,
    fragment: &graphql_syntax::FragmentDefinition,
    directive: &graphql_syntax::Directive,
) -> DiagnosticsResult<Vec<VariableDefinition>> {
    if let Some(arguments) = &directive.arguments {
        Ok(arguments
            .items
            .iter()
            .map(|variable_arg| {
                if let graphql_syntax::Value::Constant(graphql_syntax::ConstantValue::Object(
                    object,
                )) = &variable_arg.value
                {
                    let mut type_arg = None;
                    let mut default_arg = None;
                    let mut unused_local_variable_arg = None;
                    let mut provider_arg = None;
                    let mut directives_arg = None;
                    let mut extra_items = Vec::new();
                    for item in &object.items {
                        let name = item.name.value;
                        if name == *TYPE {
                            type_arg = Some(item);
                        } else if name == *DEFAULT_VALUE {
                            default_arg = Some(item);
                        } else if DirectiveName(name) == *UNUSED_LOCAL_VARIABLE_DEPRECATED {
                            unused_local_variable_arg = Some(item);
                        } else if name == *DIRECTIVES {
                            directives_arg = Some(item);
                        } else if name == *PROVIDER {
                            provider_arg = Some(item);
                        } else {
                            extra_items.push(item);
                        }
                    }
                    // Check that no extraneous keys were supplied
                    if !extra_items.is_empty() {
                        return Err(extra_items
                            .iter()
                            .map(|item| {
                                Diagnostic::error(
                                    ValidationMessage::InvalidArgumentDefinitionsKey(
                                        item.name.value,
                                    ),
                                    fragment.location.with_span(item.span),
                                )
                            })
                            .collect());
                    }

                    let variable_name = &variable_arg.name;
                    let mut directives = Vec::new();

                    // Convert variable type, validate that it's an input type
                    let type_ = get_argument_type(schema, fragment.location, type_arg, object)?;
                    if !type_.inner().is_input_type() {
                        return Err(Diagnostic::error(
                            ValidationMessage::ExpectedFragmentArgumentToHaveInputType(
                                schema.get_type_name(type_.inner()),
                            ),
                            fragment.location.with_span(variable_arg.value.span()),
                        )
                        .into());
                    }

                    if let Some(unused_local_variable_arg) = unused_local_variable_arg {
                        if !matches!(
                            unused_local_variable_arg,
                            graphql_syntax::ConstantArgument {
                                value: graphql_syntax::ConstantValue::Boolean(
                                    graphql_syntax::BooleanNode { value: true, .. }
                                ),
                                ..
                            }
                        ) {
                            return Err(vec![Diagnostic::error(
                                ValidationMessage::InvalidUnusedFragmentVariableSuppressionArg,
                                fragment
                                    .location
                                    .with_span(unused_local_variable_arg.value.span()),
                            )]);
                        }
                        directives.push(crate::Directive {
                            name: WithLocation::new(
                                fragment.location.with_span(unused_local_variable_arg.span),
                                *UNUSED_LOCAL_VARIABLE_DEPRECATED,
                            ),
                            arguments: Vec::new(),
                            data: None,
                        });
                    }

                    if let Some(provider_arg) = provider_arg {
                        let provider_module_name = provider_arg.value.get_string_literal().ok_or_else(|| {
                            vec![Diagnostic::error(
                                ValidationMessage::LiteralStringArgumentExpectedForDirective{arg_name: ArgumentName(*PROVIDER), directive_name: *ARGUMENT_DEFINITION },
                                fragment
                                    .location
                                    .with_span(provider_arg.value.span()),
                            )]
                        })?;
                        if let Some(default_arg_) = default_arg {
                            return Err(vec![Diagnostic::error(
                                ValidationMessage::ProvidedVariableIncompatibleWithDefaultValue{argument_name: variable_name.value},
                                fragment
                                    .location
                                    .with_span(provider_arg.span),
                            ).annotate("Default value declared here",
                            fragment
                            .location
                            .with_span(default_arg_.span))]);
                        }
                        directives.push(crate::Directive {
                            name: WithLocation::new(
                                fragment.location.with_span(provider_arg.span),
                                ProvidedVariableMetadata::directive_name(),
                            ),
                            arguments: Vec::new(),
                            data: Some(Box::new(ProvidedVariableMetadata {
                                module_name: provider_module_name,
                                original_variable_name: VariableName(variable_name.value),
                                fragment_source_location: fragment.location.source_location(),
                            })),
                        });
                    }

                    if let Some(directives_arg) = directives_arg {
                        if let graphql_syntax::ConstantValue::List(items) = &directives_arg.value {
                            for item in &items.items {
                                if let graphql_syntax::ConstantValue::String(directive_string) = item {
                                    let ast_directive = graphql_syntax::parse_directive(
                                        directive_string.value.lookup(),
                                        fragment.location.source_location(),
                                        // Add 1 to account for the leading quote
                                        directive_string.token.span.start + 1
                                    )?;
                                    let directive = build_directive(
                                        schema,
                                        &ast_directive,
                                        graphql_syntax::DirectiveLocation::VariableDefinition,
                                        // We currently don't have the ability to pass offset locations
                                        // to the parser call, so we first use a generated location and
                                        // later override it with an approximation.
                                        Location::generated(),
                                    )
                                    .map_err(|mut diagnostics| {
                                        for diagnostic in &mut diagnostics {
                                            diagnostic.override_location(fragment.location.with_span(directive_string.token.span));
                                        }
                                        diagnostics
                                    })?;
                                    directives.push(directive);
                                } else {
                                    return Err(vec![Diagnostic::error(
                                        ValidationMessage::ArgumentDefinitionsDirectivesNotStringListLiteral,
                                        fragment.location.with_span(item.span()),
                                    )]);
                                }
                            }
                        } else {
                            return Err(vec![Diagnostic::error(
                                ValidationMessage::ArgumentDefinitionsDirectivesNotStringListLiteral,
                                fragment.location.with_span(directives_arg.value.span()),
                            )]);
                        }
                    }

                    let default_value =
                        get_default_value(schema, fragment.location, default_arg, &type_)?;

                    Ok(VariableDefinition {
                        name: variable_name
                        .name_with_location(fragment.location.source_location()).map(VariableName),
                        type_,
                        directives,
                        default_value,
                    })
                } else {
                    Err(Diagnostic::error(
                        ValidationMessage::ExpectedArgumentDefinitionToBeObject,
                        fragment.location.with_span(variable_arg.value.span()),
                    )
                    .into())
                }
            })
            .collect::<DiagnosticsResult<Vec<VariableDefinition>>>()?)
    } else {
        Ok(Default::default())
    }
}

fn get_argument_type(
    schema: &SDLSchema,
    location: Location,
    type_arg: Option<&graphql_syntax::ConstantArgument>,
    object: &graphql_syntax::List<graphql_syntax::ConstantArgument>,
) -> DiagnosticsResult<TypeReference<Type>> {
    let type_name_and_offset = match type_arg {
        Some(graphql_syntax::ConstantArgument {
            value: graphql_syntax::ConstantValue::String(type_name_node),
            ..
        }) => Some((
            type_name_node.value,
            // Add 1 to account for the leading quote
            type_name_node.token.span.start + 1,
        )),
        Some(graphql_syntax::ConstantArgument {
            value: graphql_syntax::ConstantValue::Enum(type_name_node),
            ..
        }) => Some((type_name_node.value, type_name_node.token.span.start)),
        _ => None,
    };
    if let Some((type_name, offset)) = type_name_and_offset {
        let type_ast =
            graphql_syntax::parse_type(type_name.lookup(), location.source_location(), offset)?;
        let type_ = build_type_annotation(schema, &type_ast, location)?;
        Ok(type_)
    } else {
        Err(Diagnostic::error(
            ValidationMessage::ExpectedArgumentDefinitionLiteralType,
            location.with_span(type_arg.map_or(object.span, |x| x.span)),
        )
        .into())
    }
}

fn get_default_value(
    schema: &SDLSchema,
    location: Location,
    default_arg: Option<&graphql_syntax::ConstantArgument>,
    type_: &TypeReference<Type>,
) -> DiagnosticsResult<Option<WithLocation<ConstantValue>>> {
    default_arg
        .map(|x| {
            let constant_value_span = x.value.span();
            build_constant_value(schema, &x.value, type_, location, ValidationLevel::Strict).map(
                |constant_value| {
                    WithLocation::from_span(
                        location.source_location(),
                        constant_value_span,
                        constant_value,
                    )
                },
            )
        })
        .transpose()
}