/*
 * 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 common::DirectiveName;
use graphql_ir::Directive;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentSpread;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::ScalarField;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use schema::Schema;

use crate::util::CustomMetadataDirectives;

/// Transform to skip IR nodes if they are client-defined extensions
/// to the schema
pub fn skip_client_extensions(program: &Program) -> Program {
    let mut transform = SkipClientExtensionsTransform::new(program);
    let transformed = transform.transform_program(program);

    transformed.replace_or_else(|| program.clone())
}

struct SkipClientExtensionsTransform<'s> {
    program: &'s Program,
}

impl<'s> SkipClientExtensionsTransform<'s> {
    fn new(program: &'s Program) -> Self {
        Self { program }
    }
}

impl<'s> SkipClientExtensionsTransform<'s> {
    fn is_client_directive(&self, name: DirectiveName) -> bool {
        // Return true if:
        // - directive is a custom internal directive used to hold
        //   metadata in the IR
        // - or, directive is a client-defined directive, not present
        //   in the server schema
        CustomMetadataDirectives::is_custom_metadata_directive(name)
            || self.program.schema.is_extension_directive(name)
    }
}

impl<'s> Transformer for SkipClientExtensionsTransform<'s> {
    const NAME: &'static str = "SkipClientExtensionsTransform";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = true;

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        let transformed = self.default_transform_operation(operation);
        transformed
    }

    fn transform_fragment(
        &mut self,
        fragment: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        if self
            .program
            .schema
            .is_extension_type(fragment.type_condition)
        {
            Transformed::Delete
        } else {
            self.default_transform_fragment(fragment)
        }
    }

    fn transform_fragment_spread(&mut self, spread: &FragmentSpread) -> Transformed<Selection> {
        let fragment = self.program.fragment(spread.fragment.item).unwrap();
        if self
            .program
            .schema
            .is_extension_type(fragment.type_condition)
        {
            Transformed::Delete
        } else {
            self.default_transform_fragment_spread(spread)
        }
    }

    fn transform_inline_fragment(&mut self, fragment: &InlineFragment) -> Transformed<Selection> {
        if let Some(type_condition) = fragment.type_condition {
            if self.program.schema.is_extension_type(type_condition) {
                return Transformed::Delete;
            }
        }
        self.default_transform_inline_fragment(fragment)
    }

    fn transform_linked_field(&mut self, field: &LinkedField) -> Transformed<Selection> {
        if self
            .program
            .schema
            .field(field.definition.item)
            .is_extension
        {
            Transformed::Delete
        } else {
            self.default_transform_linked_field(field)
        }
    }

    fn transform_scalar_field(&mut self, field: &ScalarField) -> Transformed<Selection> {
        if self
            .program
            .schema
            .field(field.definition.item)
            .is_extension
        {
            Transformed::Delete
        } else {
            self.default_transform_scalar_field(field)
        }
    }

    fn transform_directive(&mut self, directive: &Directive) -> Transformed<Directive> {
        if self.is_client_directive(directive.name.item) {
            Transformed::Delete
        } else {
            self.default_transform_directive(directive)
        }
    }
}


/*
 * 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 graphql_ir::Argument;
use graphql_ir::ConstantValue;
use graphql_ir::Program;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use graphql_ir::Value;

/// Removes arguments with a `null` value. This replicates legacy Relay Compiler
/// behavior, but is not according to spec. Removing this would need some care
/// as there are small differences between not-passed and explicit null values wrt.
/// default values.
pub fn skip_null_arguments_transform(program: &Program) -> Program {
    SkipNullArgumentsTransform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

struct SkipNullArgumentsTransform;

impl Transformer for SkipNullArgumentsTransform {
    const NAME: &'static str = "SkipNullArgumentsTransform";
    const VISIT_ARGUMENTS: bool = true;
    const VISIT_DIRECTIVES: bool = true;

    fn transform_argument(&mut self, argument: &Argument) -> Transformed<Argument> {
        if matches!(argument.value.item, Value::Constant(ConstantValue::Null())) {
            Transformed::Delete
        } else {
            Transformed::Keep
        }
    }
}


/*
 * 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::sync::Arc;

use common::sync::*;
use common::NamedItem;
use common::PointerAddress;
use dashmap::DashMap;
use graphql_ir::node_identifier::NodeIdentifier;
use graphql_ir::Condition;
use graphql_ir::FragmentDefinition;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::TransformedValue;
use schema::SDLSchema;

use crate::util::is_relay_custom_inline_fragment_directive;
use crate::RelayLocationAgnosticBehavior;
use crate::DEFER_STREAM_CONSTANTS;

/**
 * A transform that removes redundant fields and fragment spreads. Redundancy is
 * defined in this context as any selection that is guaranteed to already be
 * fetched by an ancestor selection. This can occur in two cases:
 *
 * 1. Simple duplicates at the same level of the document can always be skipped:
 *
 *
 * fragment Foo on FooType {
 *   id
 *   id
 *   ...Bar
 *   ...Bar
 * }
 *
 *
 * Becomes
 *
 *
 * fragment Foo on FooType {
 *   id
 *   ...Bar
 * }
 *
 *
 * 2. Inline fragments and conditions introduce the possibility for duplication
 * at different levels of the tree. Whenever a selection is fetched in a parent,
 * it is redundant to also fetch it in a child:
 *
 *
 * fragment Foo on FooType {
 *   id
 *   ... on OtherType {
 *     id # 1
 *   }
 *   ... on FooType @include(if: $cond) {
 *     id # 2
 *   }
 * }
 *
 *
 * Becomes:
 *
 *
 * fragment Foo on FooType {
 *   id
 * }
 *
 *
 * In this example:
 * - 1 can be skipped because `id` is already fetched by the parent. Even
 *   though the type is different (FooType/OtherType), the inline fragment
 *   cannot match without the outer fragment matching so the outer `id` is
 *   guaranteed to already be fetched.
 * - 2 can be skipped for similar reasons: it doesn't matter if the condition
 *   holds, `id` is already fetched by the parent regardless.
 *
 * This transform also handles more complicated cases in which selections are
 * nested:
 *
 *
 * fragment Foo on FooType {
 *   a {
 *     bb
 *   }
 *   ... on OtherType {
 *     a {
 *       bb # 1
 *       cc
 *     }
 *   }
 *  }
 *
 *
 * Becomes
 *
 *
 * fragment Foo on FooType {
 *   a {
 *     bb
 *   }
 *   ... on OtherType {
 *     a {
 *       cc
 *     }
 *   }
 *  }
 *
 *
 * 1 can be skipped because it is already fetched at the outer level.
 */
pub fn skip_redundant_nodes(program: &Program) -> Program {
    let transform = SkipRedundantNodesTransform::new(program);
    transform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

#[derive(Default, Clone, Debug)]
struct SelectionMap(VecMap<NodeIdentifier<RelayLocationAgnosticBehavior>, Option<SelectionMap>>);

type Cache = DashMap<PointerAddress, (Transformed<Selection>, SelectionMap)>;

pub struct SkipRedundantNodesTransform {
    schema: Arc<SDLSchema>,
    cache: Cache,
}

impl<'s> SkipRedundantNodesTransform {
    fn new(program: &'_ Program) -> Self {
        Self {
            schema: Arc::clone(&program.schema),
            cache: DashMap::new(),
        }
    }

    pub fn from_schema(schema: &Arc<SDLSchema>) -> Self {
        Self {
            schema: Arc::clone(schema),
            cache: DashMap::new(),
        }
    }

    fn transform_selection(
        &self,
        selection: &Selection,
        selection_map: &mut SelectionMap,
    ) -> Transformed<Selection> {
        // This will optimize a traversal of the same subselections.
        // If it's the same node, and selection_map is empty
        // result of transform_selection has to be the same.
        let is_empty = selection_map.0.is_empty();
        let identifier =
            NodeIdentifier::from_selection(&self.schema, selection, RelayLocationAgnosticBehavior);
        match selection {
            Selection::ScalarField(_) | Selection::FragmentSpread(_) => {
                if selection_map.0.contains_key(&identifier) {
                    Transformed::Delete
                } else {
                    selection_map.0.insert(identifier, None);
                    Transformed::Keep
                }
            }
            Selection::LinkedField(selection) => {
                let should_cache = is_empty && Arc::strong_count(selection) > 2;
                if should_cache {
                    let key = PointerAddress::new(selection);
                    if let Some(cached) = self.cache.get(&key) {
                        let (cached_result, cached_selection_map) = cached.clone();
                        *selection_map = cached_selection_map;
                        return cached_result;
                    }
                }
                let result = if let Some(Some(linked_selection_map)) =
                    selection_map.0.get_mut(&identifier)
                {
                    self.transform_linked_field(selection, linked_selection_map)
                        .map(Selection::LinkedField)
                } else {
                    let mut linked_selection_map = Default::default();
                    let result = self
                        .transform_linked_field(selection, &mut linked_selection_map)
                        .map(Selection::LinkedField);
                    if !matches!(result, Transformed::Delete) {
                        selection_map
                            .0
                            .insert(identifier, Some(linked_selection_map));
                    }
                    result
                };
                if should_cache {
                    let key = PointerAddress::new(selection);
                    self.cache
                        .insert(key, (result.clone(), selection_map.clone()));
                }
                result
            }
            Selection::Condition(selection) => {
                if let Some(Some(existing_selection_map)) = selection_map.0.get_mut(&identifier) {
                    self.transform_condition(selection, existing_selection_map)
                        .map(Selection::Condition)
                } else {
                    // Fork the selection map to prevent conditional selections from
                    // affecting the outer "guaranteed" selections.
                    let mut next_selection_map = selection_map.clone();
                    let result = self
                        .transform_condition(selection, &mut next_selection_map)
                        .map(Selection::Condition);
                    if !matches!(result, Transformed::Delete) {
                        selection_map.0.insert(identifier, Some(next_selection_map));
                    }
                    result
                }
            }
            Selection::InlineFragment(selection) => {
                let should_cache = is_empty && Arc::strong_count(selection) > 2;
                if should_cache {
                    let key = PointerAddress::new(selection);
                    if let Some(cached) = self.cache.get(&key) {
                        let (cached_result, cached_selection_map) = cached.clone();
                        *selection_map = cached_selection_map;
                        return cached_result;
                    }
                }
                let result = if let Some(Some(existing_selection_map)) =
                    selection_map.0.get_mut(&identifier)
                {
                    self.transform_inline_fragment(selection, existing_selection_map)
                        .map(Selection::InlineFragment)
                } else if selection
                    .directives
                    .iter()
                    .any(is_relay_custom_inline_fragment_directive)
                {
                    let mut linked_selection_map = Default::default();
                    let result = self
                        .transform_inline_fragment(selection, &mut linked_selection_map)
                        .map(Selection::InlineFragment);
                    if !matches!(result, Transformed::Delete) {
                        selection_map
                            .0
                            .insert(identifier, Some(linked_selection_map));
                    }
                    result
                } else {
                    // Fork for inline fragments for the same reason
                    let mut next_selection_map = selection_map.clone();
                    let result = self
                        .transform_inline_fragment(selection, &mut next_selection_map)
                        .map(Selection::InlineFragment);
                    if !matches!(result, Transformed::Delete) {
                        selection_map.0.insert(identifier, Some(next_selection_map));
                    }
                    result
                };
                if should_cache {
                    let key = PointerAddress::new(selection);
                    self.cache
                        .insert(key, (result.clone(), selection_map.clone()));
                }
                result
            }
        }
    }

    fn transform_linked_field(
        &self,
        field: &LinkedField,
        selection_map: &mut SelectionMap,
    ) -> Transformed<Arc<LinkedField>> {
        let selections = self.transform_selections(&field.selections, selection_map);
        match selections {
            TransformedValue::Keep => Transformed::Keep,
            TransformedValue::Replace(selections) => {
                if selections.is_empty() {
                    Transformed::Delete
                } else {
                    Transformed::Replace(Arc::new(LinkedField {
                        selections,
                        ..field.clone()
                    }))
                }
            }
        }
    }

    fn transform_condition(
        &self,
        condition: &Condition,
        selection_map: &mut SelectionMap,
    ) -> Transformed<Arc<Condition>> {
        let selections = self.transform_selections(&condition.selections, selection_map);
        match selections {
            TransformedValue::Keep => Transformed::Keep,
            TransformedValue::Replace(selections) => {
                if selections.is_empty() {
                    Transformed::Delete
                } else {
                    Transformed::Replace(Arc::new(Condition {
                        selections,
                        ..condition.clone()
                    }))
                }
            }
        }
    }

    fn transform_inline_fragment(
        &self,
        fragment: &InlineFragment,
        selection_map: &mut SelectionMap,
    ) -> Transformed<Arc<InlineFragment>> {
        let selections = self.transform_selections(&fragment.selections, selection_map);
        match selections {
            TransformedValue::Keep => Transformed::Keep,
            TransformedValue::Replace(selections) => {
                if selections.is_empty() {
                    Transformed::Delete
                } else {
                    Transformed::Replace(Arc::new(InlineFragment {
                        selections,
                        ..fragment.clone()
                    }))
                }
            }
        }
    }

    // Mostly a copy from Transformer::transform_list, but does partition and pass down `selection_map`.
    fn transform_selections(
        &self,
        selections: &[Selection],
        selection_map: &mut SelectionMap,
    ) -> TransformedValue<Vec<Selection>> {
        if selections.is_empty() {
            return TransformedValue::Keep;
        }
        let mut result: Vec<Selection> = Vec::new();
        let mut has_changes = false;
        let selections = get_partitioned_selections(selections);

        for (index, prev_item) in selections.iter().enumerate() {
            let next_item = self.transform_selection(prev_item, selection_map);
            match next_item {
                Transformed::Keep => {
                    if has_changes {
                        result.push((*prev_item).clone());
                    }
                }
                Transformed::Delete => {
                    if !has_changes {
                        debug_assert!(result.capacity() == 0);
                        // assume most items won't be skipped and allocate space for all items
                        result.reserve(selections.len());
                        result.extend(selections.iter().take(index).map(|&x| x.clone()));
                        has_changes = true;
                    }
                }
                Transformed::Replace(next_item) => {
                    if !has_changes {
                        debug_assert!(result.capacity() == 0);
                        // assume most items won't be skipped and allocate space for all items
                        result.reserve(selections.len());
                        result.extend(selections.iter().take(index).map(|&x| x.clone()));
                        has_changes = true;
                    }
                    result.push(next_item);
                }
            }
        }
        if has_changes {
            TransformedValue::Replace(result)
        } else {
            TransformedValue::Keep
        }
    }

    pub fn transform_operation(
        &self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        let mut selection_map = Default::default();
        let selections = self.transform_selections(&operation.selections, &mut selection_map);
        match selections {
            TransformedValue::Keep => Transformed::Keep,
            TransformedValue::Replace(selections) => Transformed::Replace(OperationDefinition {
                selections,
                ..operation.clone()
            }),
        }
    }

    pub fn transform_fragment(
        &self,
        fragment: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        let mut selection_map = Default::default();
        let selections = self.transform_selections(&fragment.selections, &mut selection_map);
        match selections {
            TransformedValue::Keep => Transformed::Keep,
            TransformedValue::Replace(selections) => Transformed::Replace(FragmentDefinition {
                selections,
                ..fragment.clone()
            }),
        }
    }

    fn transform_program(&self, program: &Program) -> TransformedValue<Program> {
        let operations: Vec<Arc<OperationDefinition>> = par_iter(&program.operations)
            .filter_map(|operation| match self.transform_operation(operation) {
                Transformed::Delete => None,
                Transformed::Keep => Some(Arc::clone(operation)),
                Transformed::Replace(replacement) => Some(Arc::new(replacement)),
            })
            .collect();
        let fragments: Vec<Arc<FragmentDefinition>> = par_iter(&program.fragments)
            .filter_map(|(_, fragment)| match self.transform_fragment(fragment) {
                Transformed::Delete => None,
                Transformed::Keep => Some(Arc::clone(fragment)),
                Transformed::Replace(replacement) => Some(Arc::new(replacement)),
            })
            .collect();
        let mut next_program = Program::new(Arc::clone(&program.schema));
        for operation in operations {
            next_program.insert_operation(operation);
        }
        for fragment in fragments {
            next_program.insert_fragment(fragment);
        }
        TransformedValue::Replace(next_program)
    }
}

/* Selections are sorted with fields first, "conditionals"
 * (inline fragments & conditions) last. This means that all fields that are
 * guaranteed to be fetched are encountered prior to any duplicates that may be
 * fetched within a conditional.
 */
fn get_partitioned_selections(selections: &[Selection]) -> Vec<&Selection> {
    let mut result = Vec::with_capacity(selections.len());
    unsafe { result.set_len(selections.len()) };
    let mut non_field_index = selections
        .iter()
        .filter(|sel| is_selection_linked_or_scalar(sel))
        .count();
    let mut field_index = 0;
    for sel in selections.iter() {
        if is_selection_linked_or_scalar(sel) {
            result[field_index] = sel;
            field_index += 1;
        } else {
            result[non_field_index] = sel;
            non_field_index += 1;
        }
    }
    result
}

fn is_selection_linked_or_scalar(selection: &Selection) -> bool {
    match selection {
        Selection::LinkedField(field) => field
            .directives
            .named(DEFER_STREAM_CONSTANTS.stream_name)
            .is_none(),
        Selection::ScalarField(_) => true,
        _ => false,
    }
}

/// NOTE: intentionally local to this file, this is not a fully-general purpose
/// immutable map. see comments on methods below.
#[derive(Debug)]
struct VecMap<K, V> {
    data: Arc<Vec<(K, V)>>,
}

impl<K, V> VecMap<K, V> {
    fn new() -> Self {
        Self {
            data: Arc::new(Vec::new()),
        }
    }
}

impl<K, V> VecMap<K, V>
where
    K: Eq + Clone,
    V: Clone,
{
    fn contains_key(&self, key: &K) -> bool {
        self.data.iter().any(|(k, _v)| k == key)
    }

    fn get_mut(&mut self, key: &K) -> Option<&mut V> {
        let data = Arc::make_mut(&mut self.data);
        data.iter_mut().find(|(k, _v)| k == key).map(|(_k, v)| v)
    }

    fn insert(&mut self, key: K, value: V) {
        // NOTE: this is intentionally *not* a general-purpose insert, which should
        // update the value for the existig key if present. skip_redundant_nodes
        // always checks for the key first, so any call to insert() is guaranteed to
        // be for a non-present key. thanks to that we can bypass the existence check
        // to make insert faster.
        debug_assert!(!self.contains_key(&key));
        let data = Arc::make_mut(&mut self.data);
        data.push((key, value));
    }

    fn is_empty(&self) -> bool {
        self.data.is_empty()
    }
}

impl<K, V> Clone for VecMap<K, V> {
    fn clone(&self) -> Self {
        Self {
            data: Arc::clone(&self.data),
        }
    }
}

impl<K, V> Default for VecMap<K, V> {
    fn default() -> Self {
        Self::new()
    }
}


/*
 * 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 common::NamedItem;
use graphql_ir::FragmentDefinition;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::Transformed;
use graphql_ir::Transformer;

use crate::DIRECTIVE_SPLIT_OPERATION;

/// A transform that removes field `splitOperations`. Intended for use when e.g.
/// printing queries to send to a GraphQL server.
pub fn skip_split_operation(program: &Program) -> Program {
    let mut transform = SkipSplitOperation {};
    transform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

pub struct SkipSplitOperation;

impl Transformer for SkipSplitOperation {
    const NAME: &'static str = "SkipSplitOperationTransform";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        if operation
            .directives
            .named(*DIRECTIVE_SPLIT_OPERATION)
            .is_some()
        {
            Transformed::Delete
        } else {
            Transformed::Keep
        }
    }

    fn transform_fragment(&mut self, _: &FragmentDefinition) -> Transformed<FragmentDefinition> {
        Transformed::Keep
    }
}


/*
 * 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::sync::Arc;

use common::Diagnostic;
use common::DiagnosticsResult;
use common::NamedItem;
use graphql_ir::transform_list_multi;
use graphql_ir::Condition;
use graphql_ir::ConditionValue;
use graphql_ir::ConstantValue;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::FragmentDefinitionNameMap;
use graphql_ir::FragmentSpread;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::TransformedMulti;
use graphql_ir::TransformedValue;
use graphql_ir::Transformer;
use graphql_ir::Value;
use intern::string_key::StringKey;
use thiserror::Error;

use super::defer_stream::DEFER_STREAM_CONSTANTS;
use crate::DeferDirective;
use crate::NoInlineFragmentSpreadMetadata;
use crate::StreamDirective;

enum ValidationMode {
    Strict(Vec<Diagnostic>),
    Loose,
}

pub fn skip_unreachable_node_strict(program: &Program) -> DiagnosticsResult<Program> {
    let errors = vec![];
    let mut validation_mode = ValidationMode::Strict(errors);
    let next_program = skip_unreachable_node(program, &mut validation_mode);

    if let ValidationMode::Strict(errors) = validation_mode {
        if !errors.is_empty() {
            return Err(errors);
        }
    }
    Ok(next_program)
}

pub fn skip_unreachable_node_loose(program: &Program) -> Program {
    let mut validation_mode = ValidationMode::Loose;
    skip_unreachable_node(program, &mut validation_mode)
}

fn skip_unreachable_node(program: &Program, validation_mode: &mut ValidationMode) -> Program {
    let mut skip_unreachable_node_transform =
        SkipUnreachableNodeTransform::new(program, validation_mode);
    let transformed = skip_unreachable_node_transform.transform_program(program);

    transformed.replace_or_else(|| program.clone())
}

type VisitedFragments =
    FragmentDefinitionNameMap<(Arc<FragmentDefinition>, Transformed<FragmentDefinition>)>;

pub struct SkipUnreachableNodeTransform<'s> {
    visited_fragments: VisitedFragments,
    program: &'s Program,
    validation_mode: &'s mut ValidationMode,
}

impl<'s> Transformer for SkipUnreachableNodeTransform<'s> {
    const NAME: &'static str = "SkipUnreachableNodeTransform";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_program(&mut self, program: &Program) -> TransformedValue<Program> {
        // Iterate over the operation ASTs. Whenever we encounter a FragmentSpread (by way of
        // fn transform_fragment_spread) look up that the associated FragmentDefinition.
        // Crawl that FragmentDefinition for other FragmentSpreads and repeat.
        // Delete that FragmentSpread if the associated FragmentDefinition was deleted.
        //
        // Throughout, when we encounter a condition node with a constant predicate, either remove
        // it or replace it with its contents.
        //
        // @include(if: false)  => remove
        // @include(if: true)   => replace with contents
        // @skip(if: true)      => remove
        // @skip(if: false)     => replace with contents
        // @defer(if: false)    => replace with contents
        //
        // Removal of a condition or spread can result in a FragmentDefinition being deleted.

        let mut next_program = Program::new(Arc::clone(&program.schema));
        let mut has_changes = false;

        for operation in program.operations() {
            match self.transform_operation(operation) {
                Transformed::Delete => {
                    if let ValidationMode::Strict(errors) = &mut self.validation_mode {
                        errors.push(Diagnostic::error(
                            ValidationMessage::EmptySelectionsInDocument {
                                document: "query",
                                name: operation.name.item.0,
                            },
                            operation.name.location,
                        ));
                    }
                    has_changes = true;
                }
                Transformed::Keep => next_program.insert_operation(Arc::clone(operation)),
                Transformed::Replace(replacement) => {
                    has_changes = true;
                    next_program.insert_operation(Arc::new(replacement))
                }
            }
        }
        for fragment in program.fragments() {
            if let Some(visited_fragment) = self.visited_fragments.get(&fragment.name.item) {
                match visited_fragment {
                    (_, Transformed::Delete) => {
                        has_changes = true;
                    }
                    (fragment, Transformed::Keep) => {
                        next_program.insert_fragment(Arc::clone(fragment));
                    }
                    (_, Transformed::Replace(replacement)) => {
                        next_program.insert_fragment(Arc::new(replacement.clone()));
                        has_changes = true;
                    }
                }
            } else {
                match self.transform_fragment(fragment) {
                    Transformed::Delete => {
                        if let ValidationMode::Strict(errors) = &mut self.validation_mode {
                            errors.push(Diagnostic::error(
                                ValidationMessage::EmptySelectionsInDocument {
                                    document: "fragment",
                                    name: fragment.name.item.0,
                                },
                                fragment.name.location,
                            ));
                        }
                        has_changes = true;
                    }
                    Transformed::Keep => {
                        next_program.insert_fragment(Arc::clone(fragment));
                    }
                    Transformed::Replace(replacement) => {
                        next_program.insert_fragment(Arc::new(replacement.clone()));
                        has_changes = true;
                    }
                }
            }
        }

        if has_changes {
            TransformedValue::Replace(next_program)
        } else {
            TransformedValue::Keep
        }
    }

    fn transform_fragment(
        &mut self,
        fragment: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        // Remove the fragment with empty selections
        let selections = self.transform_selections(&fragment.selections);
        if let TransformedValue::Replace(selections) = &selections {
            if selections.is_empty() {
                return Transformed::Delete;
            }
        }
        let directives = self.transform_directives(&fragment.directives);
        if selections.should_keep() && directives.should_keep() {
            return Transformed::Keep;
        }
        Transformed::Replace(FragmentDefinition {
            directives: directives.replace_or_else(|| fragment.directives.clone()),
            selections: selections.replace_or_else(|| fragment.selections.clone()),
            ..fragment.clone()
        })
    }

    fn transform_selections(
        &mut self,
        selections: &[Selection],
    ) -> TransformedValue<Vec<Selection>> {
        transform_list_multi(selections, |selection| self.map_selection_multi(selection))
    }

    fn transform_fragment_spread(&mut self, spread: &FragmentSpread) -> Transformed<Selection> {
        if spread
            .directives
            .named(NoInlineFragmentSpreadMetadata::directive_name())
            .is_some()
        {
            return Transformed::Keep;
        }
        if self.should_delete_fragment_definition(spread.fragment.item) {
            Transformed::Delete
        } else {
            Transformed::Keep
        }
    }

    fn transform_linked_field(&mut self, field: &LinkedField) -> Transformed<Selection> {
        let transformed_field = self.default_transform_linked_field(field);
        if let Some(directive) = field.directives.named(DEFER_STREAM_CONSTANTS.stream_name) {
            if let Some(if_arg) = StreamDirective::from(directive).if_arg {
                if let Value::Constant(ConstantValue::Boolean(false)) = &if_arg.value.item {
                    let mut next_field = match transformed_field {
                        Transformed::Delete => return Transformed::Delete,
                        Transformed::Keep => Arc::new(field.clone()),
                        Transformed::Replace(Selection::LinkedField(replacement)) => replacement,
                        Transformed::Replace(other) => {
                            panic!("unexpected replacement: {:?}", other)
                        }
                    };
                    let previous_directive_len = next_field.directives.len();
                    Arc::make_mut(&mut next_field)
                        .directives
                        .retain(|directive| {
                            directive.name.item != DEFER_STREAM_CONSTANTS.stream_name
                        });
                    assert_eq!(
                        previous_directive_len,
                        next_field.directives.len() + 1,
                        "should have removed exactly one directive"
                    );
                    return Transformed::Replace(Selection::LinkedField(next_field));
                }
            }
        }
        transformed_field
    }
}

impl<'s> SkipUnreachableNodeTransform<'s> {
    fn new(program: &'s Program, validation_mode: &'s mut ValidationMode) -> Self {
        Self {
            visited_fragments: Default::default(),
            program,
            validation_mode,
        }
    }

    // Visit the fragment definition and return true if it should be deleted
    fn should_delete_fragment_definition(&mut self, key: FragmentDefinitionName) -> bool {
        if let Some((_, transformed)) = self.visited_fragments.get(&key) {
            return matches!(transformed, Transformed::Delete);
        }
        if let Some(fragment) = self.program.fragment(key) {
            self.visited_fragments
                .insert(key, (Arc::clone(fragment), Transformed::Keep));
            let transformed = self.transform_fragment(fragment);
            let should_delete = matches!(transformed, Transformed::Delete);

            // N.B. we must call self.visited_fragments.get* twice, because we cannot have
            // a reference to visited_opt and call transform_segment, which requires an
            // exclusive reference to self.
            let (_, visited_opt) = self.visited_fragments.get_mut(&key).unwrap();
            *visited_opt = transformed;
            should_delete
        } else {
            true
        }
    }

    fn map_selection_multi(&mut self, selection: &Selection) -> TransformedMulti<Selection> {
        match selection {
            Selection::FragmentSpread(selection) => {
                self.transform_fragment_spread(selection).into()
            }
            Selection::InlineFragment(selection) => self.transform_inline_fragment_multi(selection),
            Selection::LinkedField(selection) => self.transform_linked_field(selection).into(),
            Selection::ScalarField(selection) => self.transform_scalar_field(selection).into(),
            Selection::Condition(condition) => self.transform_constant_conditions(condition),
        }
    }

    fn transform_inline_fragment_multi(
        &mut self,
        inline_fragment: &InlineFragment,
    ) -> TransformedMulti<Selection> {
        if let Some(directive) = inline_fragment
            .directives
            .named(DEFER_STREAM_CONSTANTS.defer_name)
        {
            assert!(inline_fragment.directives.len() == 1);
            if let Some(if_arg) = DeferDirective::from(directive).if_arg {
                if let Value::Constant(ConstantValue::Boolean(false)) = &if_arg.value.item {
                    return TransformedMulti::ReplaceMultiple(
                        self.transform_selections(&inline_fragment.selections)
                            .replace_or_else(|| inline_fragment.selections.clone()),
                    );
                }
            }
        };
        self.default_transform_inline_fragment(inline_fragment)
            .into()
    }

    fn transform_constant_conditions(
        &mut self,
        condition: &Condition,
    ) -> TransformedMulti<Selection> {
        if let ConditionValue::Constant(b) = condition.value {
            // passing_value == "what the value needs to be in order to include the contents"
            // in other words, @skip has passing_value of false
            // and @include has passing_value of true
            //
            // so, b == condition.passing_value implies that the condition is redundant,
            // and b != condition.passing_value implies that the whole node can be removed
            let keep_contents = b == condition.passing_value;
            if keep_contents {
                // @include(if: true) or @skip(if: false)
                let contents = match self.transform_selections(&condition.selections) {
                    TransformedValue::Keep => &condition.selections,
                    TransformedValue::Replace(ref t) => t,
                }
                .clone();
                TransformedMulti::ReplaceMultiple(contents)
            } else {
                // @include(if: false) or @skip(if: true)
                TransformedMulti::Delete
            }
        } else {
            self.transform_condition(condition).into()
        }
    }
}

#[derive(Clone, Debug, Error, Eq, PartialEq, Ord, PartialOrd, Hash)]
enum ValidationMessage {
    #[error(
        "After applying transforms to the {document} `{name}` selections of \
        the `{name}` that would be sent to the server are empty. \
        This is likely due to the use of `@skip`/`@include` directives with \
        constant values that remove all selections in the {document}. "
    )]
    EmptySelectionsInDocument {
        name: StringKey,
        document: &'static str,
    },
}


/*
 * 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 graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::Transformed;
use graphql_ir::Transformer;

use crate::UPDATABLE_DIRECTIVE;

pub fn skip_updatable_queries(program: &Program) -> Program {
    let mut transform = SkipUpdatableQueries::new(program);
    let transformed = transform.transform_program(program);

    transformed.replace_or_else(|| program.clone())
}

#[allow(dead_code)]
struct SkipUpdatableQueries<'s> {
    program: &'s Program,
}

impl<'s> SkipUpdatableQueries<'s> {
    fn new(program: &'s Program) -> Self {
        Self { program }
    }
}

impl<'s> Transformer for SkipUpdatableQueries<'s> {
    const NAME: &'static str = "SkipUpdatableQueriesTransform";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = true;

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        if operation
            .directives
            .iter()
            .any(|directive| directive.name.item == *UPDATABLE_DIRECTIVE)
        {
            Transformed::Delete
        } else {
            Transformed::Keep
        }
    }
}


/*
 * 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::cmp::Ordering;
use std::collections::HashMap;

use common::PointerAddress;
use graphql_ir::transform_list;
use graphql_ir::Field;
use graphql_ir::FragmentDefinition;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::TransformedValue;
use graphql_ir::Transformer;

type Seen = HashMap<PointerAddress, Transformed<Selection>>;

/// Sorts selections in the fragments and queries (and their selections)
pub fn sort_selections(program: &Program) -> Program {
    let mut transform = SortSelectionsTransform::new(program);
    transform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

struct SortSelectionsTransform<'s> {
    seen: Seen,
    program: &'s Program,
}

impl<'s> SortSelectionsTransform<'s> {
    pub fn new(program: &'s Program) -> Self {
        Self {
            seen: Default::default(),
            program,
        }
    }
}

impl Transformer for SortSelectionsTransform<'_> {
    const NAME: &'static str = "SortSelectionsTransform";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_selections(
        &mut self,
        selections: &[Selection],
    ) -> TransformedValue<Vec<Selection>> {
        let mut next_selections =
            transform_list(selections, |selection| self.transform_selection(selection))
                .replace_or_else(|| selections.to_vec());
        next_selections.sort_unstable_by(|a, b| self.compare_selections(a, b));
        TransformedValue::Replace(next_selections)
    }

    fn transform_fragment(
        &mut self,
        fragment: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        let mut variable_definitions = fragment.variable_definitions.clone();
        let mut used_global_variables = fragment.used_global_variables.clone();
        variable_definitions.sort_unstable_by_key(|var| var.name.item);
        used_global_variables.sort_unstable_by_key(|var| var.name.item);

        let selections = self.transform_selections(&fragment.selections);
        Transformed::Replace(FragmentDefinition {
            selections: selections.replace_or_else(|| fragment.selections.clone()),
            variable_definitions,
            used_global_variables,
            ..fragment.clone()
        })
    }

    fn default_transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        self.transform_selections(&operation.selections)
            .map(|selections| OperationDefinition {
                selections,
                ..operation.clone()
            })
            .into()
    }

    fn transform_selection(&mut self, selection: &Selection) -> Transformed<Selection> {
        match selection {
            Selection::InlineFragment(selection) => {
                let key = PointerAddress::new(selection);
                if let Some(prev) = self.seen.get(&key) {
                    return prev.clone();
                }
                let transformed = self.transform_inline_fragment(selection);
                self.seen.insert(key, transformed.clone());
                transformed
            }
            Selection::LinkedField(selection) => {
                let key = PointerAddress::new(selection);
                if let Some(prev) = self.seen.get(&key) {
                    return prev.clone();
                }
                let transformed = self.transform_linked_field(selection);
                self.seen.insert(key, transformed.clone());
                transformed
            }
            Selection::Condition(selection) => {
                let key = PointerAddress::new(selection);
                if let Some(prev) = self.seen.get(&key) {
                    return prev.clone();
                }
                let transformed = self.transform_condition(selection);
                self.seen.insert(key, transformed.clone());
                transformed
            }
            Selection::ScalarField(_) => Transformed::Keep,
            Selection::FragmentSpread(_) => Transformed::Keep,
        }
    }
}

impl SortSelectionsTransform<'_> {
    fn compare_selections(&self, a: &Selection, b: &Selection) -> Ordering {
        match (a, b) {
            (Selection::ScalarField(a), Selection::ScalarField(b)) => a
                .alias_or_name(&self.program.schema)
                .cmp(&b.alias_or_name(&self.program.schema)),
            (Selection::LinkedField(a), Selection::LinkedField(b)) => a
                .alias_or_name(&self.program.schema)
                .cmp(&b.alias_or_name(&self.program.schema)),
            (Selection::FragmentSpread(a), Selection::FragmentSpread(b)) => {
                a.fragment.item.cmp(&b.fragment.item)
            }
            (Selection::InlineFragment(a), Selection::InlineFragment(b)) => {
                a.type_condition.cmp(&b.type_condition)
            }
            (Selection::Condition(a), Selection::Condition(b)) => a
                .passing_value
                .cmp(&b.passing_value)
                .then_with(|| {
                    use graphql_ir::ConditionValue::Constant;
                    use graphql_ir::ConditionValue::Variable;
                    match (&a.value, &b.value) {
                        (Constant(a), Constant(b)) => a.cmp(b),
                        (Variable(a), Variable(b)) => a.name.item.cmp(&b.name.item),
                        (Constant(_), Variable(_)) => Ordering::Less,
                        (Variable(_), Constant(_)) => Ordering::Greater,
                    }
                })
                .then_with(|| {
                    let length = a.selections.len().min(b.selections.len());
                    let a_selections = &a.selections[..length];
                    let b_selections = &b.selections[..length];
                    for i in 0..length {
                        match self.compare_selections(&a_selections[i], &b_selections[i]) {
                            Ordering::Equal => {}
                            other => return other,
                        }
                    }
                    a_selections.len().cmp(&b_selections.len())
                }),
            _ => {
                let a_ordering = selection_kind_ordering(a);
                let b_ordering = selection_kind_ordering(b);
                assert!(
                    a_ordering != b_ordering,
                    "expected different ordering, got {} == {}",
                    a_ordering,
                    b_ordering
                );
                a_ordering.cmp(&b_ordering)
            }
        }
    }
}

/// Assigns an order to different variants of Selection.
fn selection_kind_ordering(selection: &Selection) -> u8 {
    match selection {
        Selection::FragmentSpread(_) => 1,
        Selection::InlineFragment(_) => 2,
        Selection::LinkedField(_) => 3,
        Selection::ScalarField(_) => 4,
        Selection::Condition(_) => 5,
    }
}


/*
 * 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 common::ArgumentName;
use common::Diagnostic;
use common::DiagnosticsResult;
use common::DirectiveName;
use common::NamedItem;
use common::WithLocation;
use graphql_ir::ConstantArgument;
use graphql_ir::ConstantValue;
use graphql_ir::Field as IrField;
use graphql_ir::FragmentDefinition;
use graphql_ir::OperationDefinition;
use graphql_ir::OperationDefinitionName;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use indexmap::IndexMap;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use lazy_static::lazy_static;
use regex::Regex;
use schema::EnumValue;
use schema::Field;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;

use crate::create_metadata_directive;
use crate::ValidationMessage;
use crate::DIRECTIVE_SPLIT_OPERATION;

lazy_static! {
    static ref TEST_OPERATION_DIRECTIVE: DirectiveName =
        DirectiveName("relay_test_operation".intern());
    static ref TEST_OPERATION_METADATA_KEY: ArgumentName =
        ArgumentName("relayTestingSelectionTypeInfo".intern());
    static ref ENUM_VALUES_KEY: ArgumentName = ArgumentName("enumValues".intern());
    static ref NULLABLE_KEY: ArgumentName = ArgumentName("nullable".intern());
    static ref PLURAL_KEY: ArgumentName = ArgumentName("plural".intern());
    static ref TYPE_KEY: ArgumentName = ArgumentName("type".intern());
}

/// Transforms the @relay_test_operation directive to @__metadata thats printed
/// as runtime data during codegen.
/// If a `test_path_regex` is passed, only allows the directive in
/// directories matching the regex.
pub fn generate_test_operation_metadata(
    program: &Program,
    test_path_regex: &Option<Regex>,
) -> DiagnosticsResult<Program> {
    let mut transformer = GenerateTestOperationMetadata::new(program, test_path_regex);
    let next_program = transformer
        .transform_program(program)
        .replace_or_else(|| program.clone());

    if transformer.errors.is_empty() {
        Ok(next_program)
    } else {
        Err(transformer.errors)
    }
}

struct GenerateTestOperationMetadata<'a> {
    program: &'a Program,
    test_path_regex: &'a Option<Regex>,
    errors: Vec<Diagnostic>,
}

impl<'a> GenerateTestOperationMetadata<'a> {
    fn new(program: &'a Program, test_path_regex: &'a Option<Regex>) -> Self {
        GenerateTestOperationMetadata {
            program,
            test_path_regex,
            errors: Vec::new(),
        }
    }
}

impl<'a> Transformer for GenerateTestOperationMetadata<'a> {
    const NAME: &'static str = "GenerateTestOperationMetadata";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        if let Some(test_operation_directive) =
            operation.directives.named(*TEST_OPERATION_DIRECTIVE)
        {
            if let Some(test_path_regex) = self.test_path_regex {
                if !test_path_regex.is_match(operation.name.location.source_location().path()) {
                    self.errors.push(Diagnostic::error(
                        ValidationMessage::TestOperationOutsideTestDirectory {
                            test_path_regex: test_path_regex.to_string(),
                        },
                        test_operation_directive.name.location,
                    ));
                    return Transformed::Keep;
                }
            }

            let mut next_directives = Vec::with_capacity(operation.directives.len());
            for directive in &operation.directives {
                // replace @relay_test_operation with @__metadata
                if directive.name.item == *TEST_OPERATION_DIRECTIVE {
                    next_directives.push(create_metadata_directive(
                        *TEST_OPERATION_METADATA_KEY,
                        ConstantValue::Object(From::from(RelayTestOperationMetadata::new(
                            self.program,
                            &operation.selections,
                        ))),
                    ));
                } else {
                    next_directives.push(directive.clone());
                }
            }

            Transformed::Replace(OperationDefinition {
                directives: next_directives,
                ..operation.clone()
            })
        } else {
            Transformed::Keep
        }
    }

    fn transform_fragment(&mut self, _: &FragmentDefinition) -> Transformed<FragmentDefinition> {
        Transformed::Keep
    }
}

impl From<RelayTestOperationMetadata> for Vec<ConstantArgument> {
    fn from(test_metadata: RelayTestOperationMetadata) -> Self {
        let mut metadata: Vec<ConstantArgument> =
            Vec::with_capacity(test_metadata.selection_type_info.len());
        for (path, type_info) in test_metadata.selection_type_info {
            metadata.push(ConstantArgument {
                name: WithLocation::generated(ArgumentName(path)),
                value: WithLocation::generated(ConstantValue::Object(From::from(type_info))),
            })
        }
        metadata
    }
}

impl From<RelayTestOperationSelectionTypeInfo> for Vec<ConstantArgument> {
    fn from(type_info: RelayTestOperationSelectionTypeInfo) -> Self {
        vec![
            ConstantArgument {
                name: WithLocation::generated(*ENUM_VALUES_KEY),
                value: WithLocation::generated(match type_info.enum_values {
                    Some(enums) => ConstantValue::List(
                        enums
                            .iter()
                            .map(|enum_| ConstantValue::String(enum_.value))
                            .collect(),
                    ),
                    None => ConstantValue::Null(),
                }),
            },
            ConstantArgument {
                name: WithLocation::generated(*NULLABLE_KEY),
                value: WithLocation::generated(ConstantValue::Boolean(type_info.nullable)),
            },
            ConstantArgument {
                name: WithLocation::generated(*PLURAL_KEY),
                value: WithLocation::generated(ConstantValue::Boolean(type_info.plural)),
            },
            ConstantArgument {
                name: WithLocation::generated(*TYPE_KEY),
                value: WithLocation::generated(ConstantValue::String(type_info.type_)),
            },
        ]
    }
}

#[derive(Debug, Clone)]
pub struct RelayTestOperationSelectionTypeInfo {
    pub type_: StringKey,
    pub enum_values: Option<Vec<EnumValue>>,
    pub plural: bool,
    pub nullable: bool,
}

impl RelayTestOperationSelectionTypeInfo {
    fn new(schema: &SDLSchema, field: &Field) -> Self {
        let type_ = field.type_.inner();
        RelayTestOperationSelectionTypeInfo {
            type_: schema.get_type_name(type_),
            enum_values: match type_ {
                Type::Enum(enum_id) => Some(schema.enum_(enum_id).values.clone()),
                _ => None,
            },
            plural: field.type_.is_list(),
            nullable: !field.type_.is_non_null(),
        }
    }
}

#[derive(Debug)]
pub struct RelayTestOperationMetadata {
    pub selection_type_info: IndexMap<StringKey, RelayTestOperationSelectionTypeInfo>,
}

impl RelayTestOperationMetadata {
    pub fn new(program: &Program, selections: &[Selection]) -> Self {
        let schema = program.schema.as_ref();
        let mut selection_type_info: IndexMap<StringKey, RelayTestOperationSelectionTypeInfo> =
            Default::default();

        let mut processing_queue: Vec<(Option<StringKey>, &[Selection])> = vec![(None, selections)];
        while !processing_queue.is_empty() {
            if let Some(current_item) = processing_queue.pop() {
                let (path, selections) = current_item;
                for selection in selections {
                    match selection {
                        Selection::ScalarField(scalar_field) => {
                            let field = schema.field(scalar_field.definition.item);
                            let alias_or_name = scalar_field.alias_or_name(schema);
                            let next_path = next_path(path, alias_or_name);
                            selection_type_info.insert(
                                next_path,
                                RelayTestOperationSelectionTypeInfo::new(schema, field),
                            );
                        }
                        Selection::LinkedField(linked_field) => {
                            let field = schema.field(linked_field.definition.item);
                            let alias_or_name = linked_field.alias_or_name(schema);
                            let next_path = next_path(path, alias_or_name);
                            selection_type_info.insert(
                                next_path,
                                RelayTestOperationSelectionTypeInfo::new(schema, field),
                            );
                            processing_queue.push((Some(next_path), &linked_field.selections));
                        }
                        Selection::Condition(condition) => {
                            processing_queue.push((path, &condition.selections));
                        }
                        Selection::InlineFragment(inline_fragment) => {
                            processing_queue.push((path, &inline_fragment.selections));
                        }
                        Selection::FragmentSpread(spread) => {
                            // Must be a shared normalization fragment
                            let operation = program
                                .operation(OperationDefinitionName(spread.fragment.item.0))
                                .unwrap_or_else(|| {
                                    panic!("Expected fragment '{}' to exist.", spread.fragment.item)
                                });
                            assert!(
                                operation
                                    .directives
                                    .named(*DIRECTIVE_SPLIT_OPERATION)
                                    .is_some(),
                                "Expected normalization fragment spreads to reference shared normalization asts (SplitOperation)"
                            );
                            processing_queue.push((path, &operation.selections));
                        }
                    }
                }
            }
        }

        RelayTestOperationMetadata {
            selection_type_info,
        }
    }
}

fn next_path(current_path: Option<StringKey>, field_alias_or_name: StringKey) -> StringKey {
    match current_path {
        None => field_alias_or_name,
        Some(path) => format!("{}.{}", path, field_alias_or_name).intern(),
    }
}


/*
 * 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::sync::Arc;

use common::Location;
use common::NamedItem;
use common::WithLocation;
use graphql_ir::Argument;
use graphql_ir::ConstantValue;
use graphql_ir::Directive;
use graphql_ir::FragmentDefinition;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use graphql_ir::Value;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use intern::Lookup;
use schema::Schema;

use crate::connections::assert_connection_selections;
use crate::connections::build_connection_metadata;
use crate::connections::build_edge_selections;
use crate::connections::build_page_info_selections;
use crate::connections::extract_connection_directive;
use crate::connections::get_default_filters;
use crate::connections::ConnectionConstants;
use crate::connections::ConnectionInterface;
use crate::connections::ConnectionMetadata;
use crate::connections::ConnectionMetadataDirective;
use crate::defer_stream::DEFER_STREAM_CONSTANTS;
use crate::handle_fields::build_handle_field_directive_from_connection_directive;
use crate::handle_fields::KEY_ARG_NAME;

pub fn transform_connections(
    program: &Program,
    connection_interface: &ConnectionInterface,
) -> Program {
    let mut transform = ConnectionTransform::new(program, connection_interface);
    transform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

struct ConnectionTransform<'s> {
    connection_interface: &'s ConnectionInterface,
    connection_constants: ConnectionConstants,
    current_path: Option<Vec<StringKey>>,
    current_connection_metadata: Vec<ConnectionMetadata>,
    current_document_name: StringKey,
    program: &'s Program,
}

impl<'s> ConnectionTransform<'s> {
    fn new(program: &'s Program, connection_interface: &'s ConnectionInterface) -> Self {
        Self {
            connection_constants: ConnectionConstants::default(),
            connection_interface,
            current_path: None,
            current_document_name: connection_interface.cursor, // Set an arbitrary value to avoid Option
            current_connection_metadata: Vec::new(),
            program,
        }
    }

    fn transform_connection_selections(
        &mut self,
        connection_field: &LinkedField,
        connection_metadata: &ConnectionMetadata,
        connection_directive: &Directive,
    ) -> Vec<Selection> {
        let is_stream_connection = connection_directive.name.item
            == self.connection_constants.stream_connection_directive_name;
        let schema = &self.program.schema;
        let transformed_selections = self
            .transform_selections(&connection_field.selections)
            .replace_or_else(|| connection_field.selections.clone());

        let ((edges_ix, edges_field), page_info_selection) = assert_connection_selections(
            schema,
            &transformed_selections,
            self.connection_interface,
        );
        let connection_field_type = schema.field(connection_field.definition.item).type_.inner();

        // Construct edges selection
        let edges_schema_field_id = schema
            .named_field(connection_field_type, self.connection_interface.edges)
            .expect("Expected presence of edges field to have been previously validated.");
        let edges_schema_field = schema.field(edges_schema_field_id);
        let edges_field_name = edges_schema_field.name.item;
        let edge_type = edges_schema_field.type_.inner();
        let mut is_aliased_edges = false;
        let mut transformed_edges_field = if let Some(alias) = edges_field.alias {
            is_aliased_edges = true;
            // The edges selection has to be generated as non-aliased field (since product
            // code may be accessing the non-aliased response keys).
            if alias.item != edges_field_name {
                // If an alias is present, and it is different from the field name,
                // we need to build a new edges_selection
                LinkedField {
                    alias: None,
                    definition: WithLocation::generated(edges_schema_field_id),
                    arguments: Vec::new(),
                    directives: Vec::new(),
                    selections: Vec::new(),
                }
            } else {
                // Otherwise reuse the existing edges field
                edges_field.clone()
            }
        } else {
            // If there is no alias present, we can reuse the existing edges field
            edges_field.clone()
        };
        transformed_edges_field
            .selections
            .push(build_edge_selections(
                schema,
                edge_type,
                self.connection_interface,
            ));
        if is_stream_connection {
            let mut arguments = vec![];
            for arg in &connection_directive.arguments {
                if arg.name.item == DEFER_STREAM_CONSTANTS.if_arg
                    || arg.name.item == DEFER_STREAM_CONSTANTS.initial_count_arg
                    || arg.name.item == DEFER_STREAM_CONSTANTS.use_customized_batch_arg
                {
                    arguments.push(arg.clone());
                } else if arg.name.item == *KEY_ARG_NAME {
                    arguments.push(Argument {
                        name: WithLocation::new(
                            arg.name.location,
                            DEFER_STREAM_CONSTANTS.label_arg,
                        ),
                        value: arg.value.clone(),
                    });
                }
            }
            transformed_edges_field.directives.push(Directive {
                name: WithLocation::new(
                    connection_directive.name.location,
                    DEFER_STREAM_CONSTANTS.stream_name,
                ),
                arguments,
                data: None,
            });
        }

        // Construct page_info selection
        let page_info_schema_field_id = schema
            .named_field(connection_field_type, self.connection_interface.page_info)
            .expect("Expected presence of page_info field to have been previously validated.");
        let page_info_schema_field = schema.field(page_info_schema_field_id);
        let page_info_field_name = page_info_schema_field.name.item;
        let page_info_type = page_info_schema_field.type_.inner();
        let mut page_info_ix = None;
        let mut is_aliased_page_info = false;
        let mut transformed_page_info_field = match page_info_selection {
            Some((ix, page_info_field)) => {
                page_info_ix = Some(ix);
                if let Some(alias) = page_info_field.alias {
                    // The page_info selection has to be generated as non-aliased field (since product
                    // code may be accessing the non-aliased response keys).
                    if alias.item != page_info_field_name {
                        is_aliased_page_info = true;
                        // If an alias is present, and it is different from the field name,
                        // we need to build a new page_info field
                        LinkedField {
                            alias: None,
                            definition: WithLocation::generated(page_info_schema_field_id),
                            arguments: Vec::new(),
                            directives: Vec::new(),
                            selections: Vec::new(),
                        }
                    } else {
                        // Otherwise reuse the existing edges field
                        page_info_field.clone()
                    }
                } else {
                    // If there is no alias present, we can reuse the existing edges field
                    page_info_field.clone()
                }
            }
            None => LinkedField {
                alias: None,
                definition: WithLocation::generated(page_info_schema_field_id),
                arguments: Vec::new(),
                directives: Vec::new(),
                selections: Vec::new(),
            },
        };
        transformed_page_info_field
            .selections
            .push(build_page_info_selections(
                schema,
                page_info_type,
                connection_metadata,
                self.connection_constants,
                self.connection_interface,
            ));

        let transformed_page_info_field_selection = if is_stream_connection {
            let mut arguments = vec![];
            let connection_args = &connection_directive.arguments;
            if let Some(key_arg) = connection_args.named(*KEY_ARG_NAME) {
                let key = key_arg.value.item.expect_string_literal();
                arguments.push(Argument {
                    name: WithLocation::new(
                        key_arg.name.location,
                        DEFER_STREAM_CONSTANTS.label_arg,
                    ),
                    value: WithLocation::new(
                        key_arg.value.location,
                        Value::Constant(ConstantValue::String(
                            format!(
                                "{}$defer${}${}",
                                self.current_document_name,
                                key.lookup(),
                                self.connection_interface.page_info
                            )
                            .intern(),
                        )),
                    ),
                });
            }
            if let Some(if_arg) = connection_args.named(DEFER_STREAM_CONSTANTS.if_arg) {
                arguments.push(if_arg.clone());
            }
            Selection::InlineFragment(Arc::new(InlineFragment {
                type_condition: None,
                selections: vec![Selection::LinkedField(From::from(
                    transformed_page_info_field,
                ))],
                directives: vec![Directive {
                    name: WithLocation::new(
                        connection_directive.name.location,
                        DEFER_STREAM_CONSTANTS.defer_name,
                    ),
                    arguments,
                    data: None,
                }],
                spread_location: Location::generated(),
            }))
        } else {
            Selection::LinkedField(From::from(transformed_page_info_field))
        };

        // Copy the original selections, replacing edges/pageInfo (if present)
        // with the generated locations. This is to maintain the original field
        // ordering.
        let mut next_selections = transformed_selections
            .iter()
            .enumerate()
            .map(|(ix, selection)| {
                if ix == edges_ix {
                    if !is_aliased_edges {
                        return Selection::LinkedField(From::from(transformed_edges_field.clone()));
                    }
                } else if let Some(page_info_ix) = page_info_ix {
                    if ix == page_info_ix && !is_aliased_page_info {
                        return transformed_page_info_field_selection.clone();
                    }
                }
                selection.clone()
            })
            .collect::<Vec<_>>();

        // If a page_info selection didn't exist, or the selections are aliased,
        // append the generated version instead.
        if is_aliased_edges {
            next_selections.push(Selection::LinkedField(From::from(transformed_edges_field)));
        }
        if page_info_selection.is_none() || is_aliased_page_info {
            next_selections.push(transformed_page_info_field_selection);
        }
        next_selections
    }

    fn transform_connection_directives(
        &mut self,
        connection_field: &LinkedField,
        connection_directive: &Directive,
    ) -> Vec<Directive> {
        let connection_handle_directive = build_handle_field_directive_from_connection_directive(
            connection_directive,
            Some(self.connection_constants.connection_directive_name.0),
            get_default_filters(connection_field, self.connection_constants),
        );
        let mut next_directives = connection_field
            .directives
            .iter()
            .cloned()
            // Remove the original @connection directive
            .filter(|directive| directive != connection_directive)
            .collect::<Vec<_>>();

        // Add an internal (untyped) directive to pass down the connection handle
        // metadata attached to this field.
        // TODO(T63388023): Use typed directives/metadata instead
        next_directives.push(connection_handle_directive);
        next_directives
    }

    fn transform_connection_field(
        &mut self,
        connection_field: &LinkedField,
        connection_directive: &Directive,
    ) -> Transformed<Selection> {
        let connection_metadata = build_connection_metadata(
            connection_field,
            self.connection_constants,
            &self.current_path,
            connection_directive.name.item
                == self.connection_constants.stream_connection_directive_name,
        );
        let next_connection_selections = self.transform_connection_selections(
            connection_field,
            &connection_metadata,
            connection_directive,
        );
        let next_connection_directives =
            self.transform_connection_directives(connection_field, connection_directive);

        // Include the connection metadata from this linked field to
        // attach to the current root document (fragment or operation)
        self.current_connection_metadata.push(connection_metadata);

        Transformed::Replace(Selection::LinkedField(Arc::new(LinkedField {
            selections: next_connection_selections,
            directives: next_connection_directives,
            ..connection_field.clone()
        })))
    }

    fn get_metadata_directive(&mut self) -> Directive {
        ConnectionMetadataDirective(std::mem::take(&mut self.current_connection_metadata)).into()
    }
}

impl<'s> Transformer for ConnectionTransform<'s> {
    const NAME: &'static str = "ConnectionTransform";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        // TODO(T63626938): This assumes that each document is processed serially (not in parallel or concurrently)
        self.current_document_name = operation.name.item.0;
        self.current_path = Some(Vec::new());
        self.current_connection_metadata = Vec::new();

        let transformed = self.default_transform_operation(operation);
        if self.current_connection_metadata.is_empty() {
            return transformed;
        }

        let mut transformed_operation = match transformed {
            Transformed::Delete => return Transformed::Delete,
            Transformed::Keep => operation.clone(),
            Transformed::Replace(replaced) => replaced,
        };

        transformed_operation
            .directives
            .push(self.get_metadata_directive());

        Transformed::Replace(transformed_operation)
    }

    fn transform_fragment(
        &mut self,
        fragment: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        // TODO(T63626938): This assumes that each document is processed serially (not in parallel or concurrently)
        self.current_document_name = fragment.name.item.0;
        self.current_path = Some(Vec::new());
        self.current_connection_metadata = Vec::new();

        let transformed = self.default_transform_fragment(fragment);
        if self.current_connection_metadata.is_empty() {
            return transformed;
        }

        let mut transformed_fragment = match transformed {
            Transformed::Delete => return Transformed::Delete,
            Transformed::Keep => fragment.clone(),
            Transformed::Replace(replaced) => replaced,
        };

        transformed_fragment
            .directives
            .push(self.get_metadata_directive());

        Transformed::Replace(transformed_fragment)
    }

    fn transform_linked_field(&mut self, field: &LinkedField) -> Transformed<Selection> {
        let schema = &self.program.schema;
        let connection_schema_field = schema.field(field.definition.item);

        // TODO(T63626938): Shouldn't need to do this when transformer infra
        // supports passing state
        let current_path_at_field = self.current_path.clone();

        // Keep track of the current path as long as we don't encounter plural field.
        if connection_schema_field.type_.is_list() {
            self.current_path = None
        } else if let Some(path) = self.current_path.as_mut() {
            path.push(if let Some(alias) = field.alias {
                alias.item
            } else {
                connection_schema_field.name.item
            })
        }

        let res = if let Some(connection_directive) =
            extract_connection_directive(&field.directives, self.connection_constants)
        {
            self.transform_connection_field(field, connection_directive)
        } else {
            self.default_transform_linked_field(field)
        };
        self.current_path = current_path_at_field;
        res
    }
}


/*
 * 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::iter;
use std::sync::Arc;

use common::NamedItem;
use graphql_ir::FragmentSpread;
use graphql_ir::InlineFragment;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;

use crate::DEFER_STREAM_CONSTANTS;

/// Transform to unwrap selections wrapped in a InlineFragment with custom
/// directive for printing
pub fn unwrap_custom_directive_selection(program: &Program) -> Program {
    let mut transform = UnwrapCustomDirectiveSelection;
    transform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

struct UnwrapCustomDirectiveSelection;

impl Transformer for UnwrapCustomDirectiveSelection {
    const NAME: &'static str = "UnwrapCustomDirectiveSelection";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_inline_fragment(&mut self, fragment: &InlineFragment) -> Transformed<Selection> {
        if fragment.type_condition.is_none() {
            // Remove the wrapping `... @defer` for `@defer` on fragment spreads.
            let defer = fragment.directives.named(DEFER_STREAM_CONSTANTS.defer_name);
            if let Some(defer) = defer {
                if let Selection::FragmentSpread(frag_spread) = &fragment.selections[0] {
                    return Transformed::Replace(Selection::FragmentSpread(Arc::new(
                        FragmentSpread {
                            directives: frag_spread
                                .directives
                                .iter()
                                .chain(iter::once(defer))
                                .cloned()
                                .collect(),
                            fragment: frag_spread.fragment,
                            arguments: frag_spread.arguments.clone(),
                        },
                    )));
                }
            }
        }
        self.default_transform_inline_fragment(fragment)
    }
}


/*
 * 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 common::DirectiveName;
use graphql_ir::Argument;
use graphql_ir::Directive;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::ProvidedVariableMetadata;
use graphql_ir::Value;
use graphql_ir::VariableName;
use graphql_ir::ARGUMENT_DEFINITION;
use graphql_ir::UNUSED_LOCAL_VARIABLE_DEPRECATED;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use intern::Lookup;
use lazy_static::lazy_static;
use regex::Regex;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;

use crate::client_extensions::CLIENT_EXTENSION_DIRECTIVE_NAME;
use crate::connections::ConnectionMetadataDirective;
use crate::handle_fields::HANDLE_FIELD_DIRECTIVE_NAME;
use crate::inline_data_fragment::InlineDirectiveMetadata;
use crate::refetchable_fragment::RefetchableMetadata;
use crate::relay_actor_change::RELAY_ACTOR_CHANGE_DIRECTIVE_FOR_CODEGEN;
use crate::required_directive::CHILDREN_CAN_BUBBLE_METADATA_KEY;
use crate::required_directive::REQUIRED_DIRECTIVE_NAME;
use crate::ClientEdgeGeneratedQueryMetadataDirective;
use crate::ClientEdgeMetadataDirective;
use crate::FragmentAliasMetadata;
use crate::ModuleMetadata;
use crate::RefetchableDerivedFromMetadata;
use crate::RelayResolverMetadata;
use crate::RequiredMetadataDirective;
use crate::DIRECTIVE_SPLIT_OPERATION;
use crate::INTERNAL_METADATA_DIRECTIVE;

/// This function will return a new Vec[...] of directives,
/// where one will be missing. The one with `remove_directive_name` name
pub fn remove_directive(
    directives: &[Directive],
    remove_directive_name: DirectiveName,
) -> Vec<Directive> {
    let mut next_directives = Vec::with_capacity(directives.len() - 1);
    for directive in directives {
        if directive.name.item != remove_directive_name {
            next_directives.push(directive.clone());
        }
    }
    next_directives
}

/// Function will create a new Vec[...] of directives
/// when one of them will be replaced with the `replacement`. If the name of
/// `replacement` is matched with the item in the list
pub fn replace_directive(directives: &[Directive], replacement: Directive) -> Vec<Directive> {
    directives
        .iter()
        .map(|directive| {
            if directive.name.item == replacement.name.item {
                return replacement.to_owned();
            }
            directive.to_owned()
        })
        .collect()
}

/// The function that will return a variable name for an argument
/// it it uses a variable (and it the argument is available)
pub fn extract_variable_name(argument: Option<&Argument>) -> Option<StringKey> {
    match argument {
        Some(arg) => match &arg.value.item {
            Value::Variable(var) => Some(var.name.item.0),
            _ => None,
        },
        None => None,
    }
}

lazy_static! {
    static ref CUSTOM_METADATA_DIRECTIVES: [DirectiveName; 19] = [
        *CLIENT_EXTENSION_DIRECTIVE_NAME,
        ConnectionMetadataDirective::directive_name(),
        *HANDLE_FIELD_DIRECTIVE_NAME,
        ModuleMetadata::directive_name(),
        *DIRECTIVE_SPLIT_OPERATION,
        RefetchableMetadata::directive_name(),
        RefetchableDerivedFromMetadata::directive_name(),
        *INTERNAL_METADATA_DIRECTIVE,
        *ARGUMENT_DEFINITION,
        *REQUIRED_DIRECTIVE_NAME,
        RequiredMetadataDirective::directive_name(),
        ClientEdgeMetadataDirective::directive_name(),
        ClientEdgeGeneratedQueryMetadataDirective::directive_name(),
        *CHILDREN_CAN_BUBBLE_METADATA_KEY,
        RelayResolverMetadata::directive_name(),
        *UNUSED_LOCAL_VARIABLE_DEPRECATED,
        *RELAY_ACTOR_CHANGE_DIRECTIVE_FOR_CODEGEN,
        ProvidedVariableMetadata::directive_name(),
        FragmentAliasMetadata::directive_name(),
    ];
    static ref DIRECTIVES_SKIPPED_IN_NODE_IDENTIFIER: [DirectiveName; 8] = [
        *CLIENT_EXTENSION_DIRECTIVE_NAME,
        ConnectionMetadataDirective::directive_name(),
        *HANDLE_FIELD_DIRECTIVE_NAME,
        RefetchableMetadata::directive_name(),
        RefetchableDerivedFromMetadata::directive_name(),
        *INTERNAL_METADATA_DIRECTIVE,
        *ARGUMENT_DEFINITION,
        *REQUIRED_DIRECTIVE_NAME,
    ];
    static ref RELAY_CUSTOM_INLINE_FRAGMENT_DIRECTIVES: [DirectiveName; 8] = [
        *CLIENT_EXTENSION_DIRECTIVE_NAME,
        ModuleMetadata::directive_name(),
        InlineDirectiveMetadata::directive_name(),
        *RELAY_ACTOR_CHANGE_DIRECTIVE_FOR_CODEGEN,
        ClientEdgeMetadataDirective::directive_name(),
        DirectiveName("defer".intern()),
        FragmentAliasMetadata::directive_name(),
        RelayResolverMetadata::directive_name(),
    ];
    static ref VALID_PROVIDED_VARIABLE_NAME: Regex = Regex::new(r#"^[A-Za-z0-9_]*$"#).unwrap();
    pub static ref INTERNAL_RELAY_VARIABLES_PREFIX: StringKey = "__relay_internal".intern();
}

pub struct CustomMetadataDirectives;

impl CustomMetadataDirectives {
    pub fn is_custom_metadata_directive(name: DirectiveName) -> bool {
        CUSTOM_METADATA_DIRECTIVES.contains(&name)
    }

    pub fn should_skip_in_node_identifier(name: DirectiveName) -> bool {
        DIRECTIVES_SKIPPED_IN_NODE_IDENTIFIER.contains(&name)
    }

    pub fn is_handle_field_directive(name: DirectiveName) -> bool {
        name == *HANDLE_FIELD_DIRECTIVE_NAME
    }
}

pub fn is_relay_custom_inline_fragment_directive(directive: &Directive) -> bool {
    RELAY_CUSTOM_INLINE_FRAGMENT_DIRECTIVES.contains(&directive.name.item)
}

pub fn generate_abstract_type_refinement_key(schema: &SDLSchema, type_: Type) -> StringKey {
    format!("__is{}", schema.get_type_name(type_).lookup()).intern()
}

pub fn get_normalization_operation_name(name: StringKey) -> String {
    format!("{}$normalization", name)
}

pub fn get_fragment_filename(fragment_name: FragmentDefinitionName) -> StringKey {
    format!(
        "{}.graphql",
        get_normalization_operation_name(fragment_name.0)
    )
    .intern()
}

pub fn format_provided_variable_name(module_name: StringKey) -> VariableName {
    let x = if VALID_PROVIDED_VARIABLE_NAME.is_match(module_name.lookup()) {
        format!(
            "{}__pv__{}",
            *INTERNAL_RELAY_VARIABLES_PREFIX,
            module_name.lookup()
        )
        .intern()
    } else {
        let transformed_name = module_name
            .lookup()
            .chars()
            .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
            .collect::<String>();
        format!(
            "{}__pv__{}",
            *INTERNAL_RELAY_VARIABLES_PREFIX, transformed_name
        )
        .intern()
    };
    VariableName(x)
}


/*
 * 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 common::Diagnostic;
use common::DiagnosticsResult;
use graphql_ir::FragmentDefinition;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use graphql_ir::ValidationMessage;
use schema::Schema;

use crate::root_variables::InferVariablesVisitor;

/// Transform that validates/updates operation variable definitions:
/// - Removes unused variable definitions. Variables can become dynamically unused due to
///   fragment-local variables, so it's convenient to automatically remove unused variables
///   rather than erroring.
/// - Reports errors if operation variables are defined with an incompatible type to how they
///   are used. For example, a `Boolean` type may not flow into a `Boolean!` argument.
///
/// NOTE: It would likely make sense to generalize this further, with options to control
/// what actions the transform should take for the following situations:
/// - Incompatible variable types: whether to fix (update type), error, or ignore
/// - Unused variables: whether to fix (remove), error, or ignore
/// - Missing variables: whether to fix (add variable), error, or ignore
pub fn validate_operation_variables(program: &Program) -> DiagnosticsResult<Program> {
    let mut transform = ValidateOperationVariables::new(program);
    let program = transform
        .transform_program(program)
        .replace_or_else(|| program.clone());
    if !transform.errors.is_empty() {
        Err(transform.errors)
    } else {
        Ok(program)
    }
}

pub struct ValidateOperationVariables<'s> {
    errors: Vec<Diagnostic>,
    program: &'s Program,
    visitor: InferVariablesVisitor<'s>,
}

impl<'s> ValidateOperationVariables<'s> {
    fn new(program: &'s Program) -> Self {
        Self {
            errors: Default::default(),
            program,
            visitor: InferVariablesVisitor::new(program),
        }
    }
}

/// Refines the argument definitions for operations to remove unused arguments
/// due to statically pruned conditional branches (e.g. because of overriding
/// a variable used in `@include()` to be false).
impl<'s> Transformer for ValidateOperationVariables<'s> {
    const NAME: &'static str = "ValidateOperationVariables";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        let (variables, errors) = self.visitor.infer_operation_variables(operation);
        self.errors.extend(errors);
        let schema = &self.program.schema;
        let mut has_unused_variable = false;
        for definition in &operation.variable_definitions {
            if definition.type_.is_non_null() && definition.has_non_null_default_value() {
                self.errors.push(Diagnostic::error(
                    ValidationMessage::NonNullableVariableHasDefaultValue {
                        variable_name: definition.name.item,
                    },
                    definition
                        .default_value
                        .as_ref()
                        .map(|default_value| default_value.location)
                        .unwrap_or(definition.name.location),
                ));
                continue;
            }

            if let Some(variable_usage) = variables.get(&definition.name.item) {
                // The effective type of the variable when taking into account its default value:
                // if there is a non-null default then the value's type is non-null.
                let non_null_type = definition.type_.non_null();
                let effective_type = if definition.has_non_null_default_value() {
                    &non_null_type
                } else {
                    &definition.type_
                };
                if !schema.is_type_subtype_of(effective_type, &variable_usage.type_) {
                    self.errors.push(
                        Diagnostic::error(
                            ValidationMessage::InvalidVariableUsage {
                                defined_type: schema.get_type_string(&definition.type_),
                                used_type: schema.get_type_string(&variable_usage.type_),
                            },
                            definition.name.location,
                        )
                        .annotate(
                            format!(
                                "Variable is used as '{}'",
                                schema.get_type_string(&variable_usage.type_)
                            ),
                            variable_usage.name.location,
                        ),
                    );
                }
            } else {
                has_unused_variable = true;
            }
        }

        if has_unused_variable {
            let next_variables = operation
                .variable_definitions
                .iter()
                .filter(|var| variables.contains_key(&var.name.item))
                .cloned()
                .collect();
            Transformed::Replace(OperationDefinition {
                variable_definitions: next_variables,
                ..operation.clone()
            })
        } else {
            Transformed::Keep
        }
    }

    fn transform_fragment(&mut self, _: &FragmentDefinition) -> Transformed<FragmentDefinition> {
        Transformed::Keep
    }
}