/*
 * 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::Span;
use graphql_ir::FragmentDefinitionName;
use graphql_syntax::ExecutableDocument;
use intern::string_key::StringKey;
use resolution_path::IdentParent;
use resolution_path::IdentPath;
use resolution_path::LinkedFieldPath;
use resolution_path::ResolutionPath;
use resolution_path::ResolvePosition;
use resolution_path::ScalarFieldPath;
use resolution_path::SelectionParent;
use resolution_path::TypeConditionPath;
use schema::SDLSchema;

use super::DefinitionDescription;
use crate::lsp_runtime_error::LSPRuntimeError;
use crate::lsp_runtime_error::LSPRuntimeResult;

pub fn get_graphql_definition_description(
    document: ExecutableDocument,
    position_span: Span,
    schema: &Arc<SDLSchema>,
) -> LSPRuntimeResult<DefinitionDescription> {
    let node_path = document.resolve((), position_span);
    match node_path {
        ResolutionPath::Ident(IdentPath {
            inner: fragment_name,
            parent: IdentParent::FragmentSpreadName(_),
        }) => Ok(DefinitionDescription::Fragment {
            fragment_name: FragmentDefinitionName(fragment_name.value),
        }),
        ResolutionPath::Ident(IdentPath {
            inner: field_name,
            parent:
                IdentParent::LinkedFieldName(LinkedFieldPath {
                    inner: _,
                    parent: selection_path,
                }),
        }) => resolve_field(field_name.value, selection_path.parent, schema),
        ResolutionPath::Ident(IdentPath {
            inner: field_name,
            parent:
                IdentParent::ScalarFieldName(ScalarFieldPath {
                    inner: _,
                    parent: selection_path,
                }),
        }) => resolve_field(field_name.value, selection_path.parent, schema),
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent:
                IdentParent::TypeConditionType(TypeConditionPath {
                    inner: type_condition,
                    parent: _,
                }),
        }) => Ok(DefinitionDescription::Type {
            type_name: type_condition.type_.value,
        }),
        _ => Err(LSPRuntimeError::ExpectedError),
    }
}

fn resolve_field(
    field_name: StringKey,
    selection_parent: SelectionParent<'_>,
    schema: &Arc<SDLSchema>,
) -> LSPRuntimeResult<DefinitionDescription> {
    let parent_type = selection_parent
        .find_parent_type(schema)
        .ok_or(LSPRuntimeError::ExpectedError)?;

    Ok(DefinitionDescription::Field {
        parent_type,
        field_name,
    })
}


/*
 * 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.
 */

//! Utilities for providing the goto definition feature

mod goto_docblock_definition;
mod goto_graphql_definition;
use std::str;
use std::sync::Arc;

use graphql_ir::FragmentDefinitionName;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use lsp_types::request::GotoDefinition;
use lsp_types::request::Request;
use lsp_types::GotoDefinitionResponse;
use lsp_types::Url;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;
use serde::Deserialize;
use serde::Serialize;

use self::goto_docblock_definition::get_docblock_definition_description;
use self::goto_graphql_definition::get_graphql_definition_description;
use crate::location::transform_relay_location_to_lsp_location;
use crate::lsp_runtime_error::LSPRuntimeError;
use crate::lsp_runtime_error::LSPRuntimeResult;
use crate::server::GlobalState;
use crate::FieldDefinitionSourceInfo;
use crate::FieldSchemaInfo;
use crate::LSPExtraDataProvider;

/// A concrete description of a GraphQL definition that a user would like to goto.
pub enum DefinitionDescription {
    Field {
        parent_type: Type,
        field_name: StringKey,
    },
    Fragment {
        fragment_name: FragmentDefinitionName,
    },
    Type {
        type_name: StringKey,
    },
}

/// Resolve a GotoDefinitionRequest to a GotoDefinitionResponse
pub fn on_goto_definition(
    state: &impl GlobalState,
    params: <GotoDefinition as Request>::Params,
) -> LSPRuntimeResult<<GotoDefinition as Request>::Result> {
    let (feature, position_span) =
        state.extract_feature_from_text(&params.text_document_position_params, 1)?;

    let project_name = state
        .extract_project_name_from_url(&params.text_document_position_params.text_document.uri)?;
    let schema = state.get_schema(&project_name)?;
    let program = state.get_program(&project_name)?;

    let definition_description = match feature {
        crate::Feature::GraphQLDocument(document) => {
            get_graphql_definition_description(document, position_span, &schema)?
        }
        crate::Feature::DocblockIr(docblock_ir) => {
            get_docblock_definition_description(&docblock_ir, position_span)?
        }
    };

    let extra_data_provider = &*state.get_extra_data_provider();
    let root_dir = state.root_dir();

    let goto_definition_response: GotoDefinitionResponse = match definition_description {
        DefinitionDescription::Field {
            parent_type,
            field_name,
        } => locate_field_definition(
            &schema,
            parent_type,
            field_name,
            extra_data_provider,
            project_name,
            &root_dir,
        )?,
        DefinitionDescription::Fragment { fragment_name } => {
            locate_fragment_definition(program, fragment_name, &root_dir)?
        }
        DefinitionDescription::Type { type_name } => locate_type_definition(
            extra_data_provider,
            project_name,
            type_name,
            &schema,
            &root_dir,
        )?,
    };

    // For some lsp-clients, such as clients relying on org.eclipse.lsp4j,
    // (see https://javadoc.io/static/org.eclipse.lsp4j/org.eclipse.lsp4j/0.8.1/org/eclipse/lsp4j/services/TextDocumentService.html)
    // the definition response should be vector of location or locationlink.
    // Therefore, let's convert the GotoDefinitionResponse::Scalar into Vector
    if let GotoDefinitionResponse::Scalar(l) = goto_definition_response {
        return Ok(Some(GotoDefinitionResponse::Array(vec![l])));
    }

    Ok(Some(goto_definition_response))
}

fn locate_fragment_definition(
    program: graphql_ir::Program,
    fragment_name: FragmentDefinitionName,
    root_dir: &std::path::Path,
) -> Result<GotoDefinitionResponse, LSPRuntimeError> {
    let fragment = program.fragment(fragment_name).ok_or_else(|| {
        LSPRuntimeError::UnexpectedError(format!(
            "Could not find fragment with name {}",
            fragment_name
        ))
    })?;
    Ok(GotoDefinitionResponse::Scalar(
        transform_relay_location_to_lsp_location(root_dir, fragment.name.location)?,
    ))
}

fn locate_type_definition(
    extra_data_provider: &dyn LSPExtraDataProvider,
    project_name: StringKey,
    type_name: StringKey,
    schema: &Arc<SDLSchema>,
    root_dir: &std::path::Path,
) -> Result<GotoDefinitionResponse, LSPRuntimeError> {
    let provider_response = extra_data_provider.resolve_field_definition(
        project_name.to_string(),
        type_name.to_string(),
        None,
    );

    let field_definition_source_info = get_field_definition_source_info_result(provider_response);

    match field_definition_source_info {
        Ok(source_info) => Ok(if source_info.is_local {
            GotoDefinitionResponse::Scalar(get_location(
                &source_info.file_path,
                source_info.line_number,
            )?)
        } else {
            return Err(LSPRuntimeError::ExpectedError);
        }),
        // If we couldn't resolve through the extra data provider, we'll fallback to
        // try to find a location in the server sdl.
        Err(_) => {
            let type_ = schema.get_type(type_name);

            type_
                .map(|type_| match type_ {
                    Type::InputObject(input_object_id) => {
                        schema.input_object(input_object_id).name.location
                    }
                    Type::Enum(enum_id) => schema.enum_(enum_id).name.location,
                    Type::Interface(interface_id) => schema.interface(interface_id).name.location,
                    Type::Scalar(scalar_id) => schema.scalar(scalar_id).name.location,
                    Type::Union(union_id) => schema.union(union_id).name.location,
                    Type::Object(object_id) => schema.object(object_id).name.location,
                })
                .map(|schema_location| {
                    transform_relay_location_to_lsp_location(root_dir, schema_location)
                        .map(GotoDefinitionResponse::Scalar)
                })
                .ok_or(LSPRuntimeError::ExpectedError)?
        }
    }
}

fn locate_field_definition(
    schema: &Arc<SDLSchema>,
    parent_type: Type,
    field_name: StringKey,
    extra_data_provider: &dyn LSPExtraDataProvider,
    project_name: StringKey,
    root_dir: &std::path::Path,
) -> Result<GotoDefinitionResponse, LSPRuntimeError> {
    let field = schema.field(schema.named_field(parent_type, field_name).ok_or_else(|| {
        LSPRuntimeError::UnexpectedError(format!("Could not find field with name {}", field_name,))
    })?);
    let parent_type = schema.get_type_name(parent_type);
    let provider_response = extra_data_provider.resolve_field_definition(
        project_name.to_string(),
        parent_type.to_string(),
        Some(FieldSchemaInfo {
            name: field_name.to_string(),
            is_extension: field.is_extension,
        }),
    );
    Ok(if let Ok(Some(source_info)) = provider_response {
        // Step 1: does extra_data_provider know anything about this field?
        if source_info.is_local {
            GotoDefinitionResponse::Scalar(get_location(
                &source_info.file_path,
                source_info.line_number,
            )?)
        } else {
            return Err(LSPRuntimeError::ExpectedError);
        }
    } else if let Ok(location) =
        transform_relay_location_to_lsp_location(root_dir, field.name.location)
    {
        // Step 2: is field a standalone graphql file?
        GotoDefinitionResponse::Scalar(location)
    } else {
        // Give up
        return Err(LSPRuntimeError::ExpectedError);
    })
}

fn get_location(path: &str, line: u64) -> Result<lsp_types::Location, LSPRuntimeError> {
    let start = lsp_types::Position {
        line: line as u32,
        character: 0,
    };
    let range = lsp_types::Range { start, end: start };

    let uri = Url::parse(&format!("file://{}", path)).map_err(|e| {
        LSPRuntimeError::UnexpectedError(format!("Could not parse path as URL: {}", e))
    })?;

    Ok(lsp_types::Location { uri, range })
}

pub(crate) enum GetSourceLocationOfTypeDefinition {}

#[derive(Deserialize, Serialize)]
pub(crate) struct GetSourceLocationOfTypeDefinitionParams {
    type_name: String,
    field_name: Option<String>,
    schema_name: String,
}

#[derive(Deserialize, Serialize)]
pub(crate) struct GetSourceLocationOfTypeDefinitionResult {
    field_definition_source_info: FieldDefinitionSourceInfo,
}

impl Request for GetSourceLocationOfTypeDefinition {
    type Params = GetSourceLocationOfTypeDefinitionParams;
    type Result = GetSourceLocationOfTypeDefinitionResult;
    const METHOD: &'static str = "relay/getSourceLocationOfTypeDefinition";
}

// Specific to schema explorer.
pub(crate) fn on_get_source_location_of_type_definition(
    state: &impl GlobalState,
    params: <GetSourceLocationOfTypeDefinition as Request>::Params,
) -> LSPRuntimeResult<<GetSourceLocationOfTypeDefinition as Request>::Result> {
    let schema = state.get_schema(&(&params.schema_name as &str).intern())?;

    let type_ = schema
        .get_type((&params.type_name as &str).intern())
        .ok_or_else(|| {
            LSPRuntimeError::UnexpectedError(format!(
                "Could not find type with name {}",
                &params.type_name
            ))
        })?;

    let field_info = params
        .field_name
        .map(|field_name| {
            schema
                .named_field(type_, (&field_name as &str).intern())
                .ok_or_else(|| {
                    LSPRuntimeError::UnexpectedError(format!(
                        "Could not find field with name {}",
                        field_name
                    ))
                })
        })
        .transpose()?
        .map(|field_id| {
            let field = schema.field(field_id);
            FieldSchemaInfo {
                name: field.name.item.to_string(),
                is_extension: field.is_extension,
            }
        });

    // TODO add go-to-definition for client fields
    let field_definition_source_info = get_field_definition_source_info_result(
        state.get_extra_data_provider().resolve_field_definition(
            params.schema_name,
            params.type_name,
            field_info,
        ),
    )?;

    Ok(GetSourceLocationOfTypeDefinitionResult {
        field_definition_source_info,
    })
}

fn get_field_definition_source_info_result(
    result: Result<Option<FieldDefinitionSourceInfo>, String>,
) -> LSPRuntimeResult<FieldDefinitionSourceInfo> {
    result
        .map_err(LSPRuntimeError::UnexpectedError)?
        .ok_or_else(|| {
            LSPRuntimeError::UnexpectedError(
                "Expected result when resolving field definition location".to_string(),
            )
        })
}


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

use common::PerfLogger;
use common::SourceLocationKey;
use graphql_ir::build_ir_with_extra_features;
use graphql_ir::BuilderOptions;
use graphql_ir::ExecutableDefinition;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::FragmentVariablesSemantic;
use graphql_ir::OperationDefinition;
use graphql_ir::OperationDefinitionName;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_syntax::parse_executable_with_error_recovery;
use graphql_text_printer::print_full_operation;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use lsp_types::request::Request;
use lsp_types::Url;
use relay_compiler::config::ProjectConfig;
use relay_transforms::apply_transforms;
use relay_transforms::CustomTransformsConfig;
use relay_transforms::Programs;
use schema::SDLSchema;
use schema_documentation::SchemaDocumentation;
use serde::Deserialize;
use serde::Serialize;

use crate::lsp_runtime_error::LSPRuntimeResult;
use crate::server::GlobalState;
use crate::server::LSPState;
use crate::LSPRuntimeError;

pub(crate) enum GraphQLExecuteQuery {}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct GraphQLExecuteQueryParams {
    text: String,
    document_path: Option<String>,
    schema_name: Option<String>,
}

impl GraphQLExecuteQueryParams {
    fn get_url(&self) -> Option<Url> {
        if let Some(path) = &self.document_path {
            Url::parse(&format!("file://{}", path)).ok()
        } else {
            None
        }
    }

    fn get_schema_name(&self) -> StringKey {
        if let Some(schema_name) = &self.schema_name {
            schema_name.clone().intern()
        } else {
            "facebook".intern()
        }
    }
}

impl Request for GraphQLExecuteQuery {
    type Params = GraphQLExecuteQueryParams;
    type Result = String;
    const METHOD: &'static str = "graphql/executeQuery";
}

/// This function will return the program that contains only operation
/// and all referenced fragments.
/// We can use it to print the full query text
fn get_operation_only_program(
    operation: Arc<OperationDefinition>,
    fragments: Vec<Arc<FragmentDefinition>>,
    program: &Program,
) -> Option<Program> {
    let mut selections_to_visit: Vec<_> = vec![&operation.selections];
    let mut next_program = Program::new(program.schema.clone());
    next_program.insert_operation(Arc::clone(&operation));
    for fragment in fragments.iter() {
        selections_to_visit.push(&fragment.selections);
        next_program.insert_fragment(Arc::clone(fragment));
    }

    let mut visited_fragments: HashSet<FragmentDefinitionName> = HashSet::default();

    while !selections_to_visit.is_empty() {
        let current_selections = selections_to_visit.pop()?;
        for selection in current_selections {
            match selection {
                Selection::FragmentSpread(spread) => {
                    // Skip, if we already visited this fragment
                    if visited_fragments.contains(&spread.fragment.item) {
                        continue;
                    }
                    visited_fragments.insert(spread.fragment.item);
                    // Second, if this fragment is already in the `next_program`,
                    // it selection already added to the visiting stack
                    if next_program.fragment(spread.fragment.item).is_some() {
                        continue;
                    }

                    // Finally, add all missing fragment spreads from the full program
                    let fragment = program.fragment(spread.fragment.item)?;

                    selections_to_visit.push(&fragment.selections);
                    next_program.insert_fragment(Arc::clone(fragment));
                }
                Selection::Condition(condition) => {
                    selections_to_visit.push(&condition.selections);
                }
                Selection::LinkedField(linked_field) => {
                    selections_to_visit.push(&linked_field.selections);
                }
                Selection::InlineFragment(inline_fragment) => {
                    selections_to_visit.push(&inline_fragment.selections);
                }
                Selection::ScalarField(_) => {}
            }
        }
    }

    Some(next_program)
}

/// Given the `Program` that contain operation+all its fragments this
/// function will `apply_transforms` and create a full set of programs, including the one
/// that may generate full operation text
fn transform_program<TPerfLogger: PerfLogger + 'static>(
    project_config: &ProjectConfig,
    program: Arc<Program>,
    perf_logger: Arc<TPerfLogger>,
    custom_transforms_config: Option<&CustomTransformsConfig>,
) -> Result<Programs, String> {
    apply_transforms(
        project_config,
        program,
        Default::default(),
        perf_logger,
        None,
        custom_transforms_config,
    )
    .map_err(|errors| format!("{:?}", errors))
}

fn print_full_operation_text(programs: Programs, operation_name: StringKey) -> Option<String> {
    let print_operation_node = programs
        .operation_text
        .operation(OperationDefinitionName(operation_name))?;

    Some(print_full_operation(
        &programs.operation_text,
        print_operation_node,
        Default::default(),
    ))
}

/// From the list of AST nodes we're trying to extract the operation and possible
/// list of fragment, to construct the initial `Program` that we could later transform
/// and print
fn build_operation_ir_with_fragments(
    definitions: &[graphql_syntax::ExecutableDefinition],
    schema: Arc<SDLSchema>,
) -> Result<(Arc<OperationDefinition>, Vec<Arc<FragmentDefinition>>), String> {
    let ir = build_ir_with_extra_features(
        &schema,
        definitions,
        &BuilderOptions {
            allow_undefined_fragment_spreads: true,
            fragment_variables_semantic: FragmentVariablesSemantic::PassedValue,
            relay_mode: Some(graphql_ir::RelayMode),
            default_anonymous_operation_name: Some("anonymous".intern()),
        },
    )
    .map_err(|errors| format!("{:?}", errors))?;

    if let Some(operation) = ir.iter().find_map(|item| {
        if let ExecutableDefinition::Operation(operation) = item {
            Some(Arc::new(operation.clone()))
        } else {
            None
        }
    }) {
        let fragments = ir
            .iter()
            .filter_map(|item| {
                if let ExecutableDefinition::Fragment(fragment) = item {
                    Some(Arc::new(fragment.clone()))
                } else {
                    None
                }
            })
            .collect::<Vec<_>>();

        Ok((operation, fragments))
    } else {
        Err("Unable to find an operation.".to_string())
    }
}

pub(crate) fn get_query_text<
    TPerfLogger: PerfLogger + 'static,
    TSchemaDocumentation: SchemaDocumentation,
>(
    state: &LSPState<TPerfLogger, TSchemaDocumentation>,
    original_text: String,
    project_name: &StringKey,
) -> LSPRuntimeResult<String> {
    let schema = state.get_schema(project_name)?;

    let project_config = state
        .config
        .enabled_projects()
        .find(|project_config| &project_config.name == project_name)
        .ok_or_else(|| {
            LSPRuntimeError::UnexpectedError(format!(
                "Unable to get project config for project {}.",
                project_name
            ))
        })?;

    let result = parse_executable_with_error_recovery(&original_text, SourceLocationKey::Generated);

    if !&result.diagnostics.is_empty() {
        return Err(LSPRuntimeError::UnexpectedError(
            result
                .diagnostics
                .iter()
                .map(|err| format!("- {}\n", err))
                .collect::<String>(),
        ));
    }

    let (operation, fragments) =
        build_operation_ir_with_fragments(&result.item.definitions, schema)
            .map_err(LSPRuntimeError::UnexpectedError)?;

    let operation_name = operation.name.item.0;
    let program = state.get_program(project_name)?;

    let query_text =
        if let Some(program) = get_operation_only_program(operation, fragments, &program) {
            let programs = transform_program(
                project_config,
                Arc::new(program),
                Arc::clone(&state.perf_logger),
                state.config.custom_transforms.as_ref(),
            )
            .map_err(LSPRuntimeError::UnexpectedError)?;

            print_full_operation_text(programs, operation_name).unwrap_or(original_text)
        } else {
            original_text
        };

    Ok(query_text)
}

pub(crate) fn on_graphql_execute_query(
    state: &impl GlobalState,
    params: GraphQLExecuteQueryParams,
) -> LSPRuntimeResult<<GraphQLExecuteQuery as Request>::Result> {
    let project_name = if let Some(url) = &params.get_url() {
        state
            .extract_project_name_from_url(url)
            .unwrap_or_else(|_| params.get_schema_name())
    } else {
        params.get_schema_name()
    };

    state.get_full_query_text(params.text, &project_name)
}


/*
 * 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.
 */

//! Utilities for providing the hover feature

use intern::Lookup;
use lsp_types::request::HoverRequest;
use lsp_types::request::Request;
use lsp_types::LanguageString;
use lsp_types::MarkedString;
use resolution_path::ResolvePosition;
use serde::Serialize;

use crate::lsp_runtime_error::LSPRuntimeResult;
use crate::server::GlobalState;

mod with_resolution_path;
pub use with_resolution_path::get_hover;

pub use self::with_resolution_path::ContentConsumerType;

fn graphql_marked_string(value: String) -> MarkedString {
    MarkedString::LanguageString(LanguageString {
        language: "graphql".to_string(),
        value,
    })
}

/// This will provide a more accurate information about some of the specific Relay directives
/// that cannot be expressed via SDL
fn argument_definition_hover_info(directive_name: &str) -> Option<MarkedString> {
    match directive_name {
        "argumentDefinitions" => Some(
            r#"
`@argumentDefinitions` is a directive used to specify arguments taken by a fragment.

---
@see: https://relay.dev/docs/en/graphql-in-relay.html#argumentdefinitions
"#,
        ),
        "arguments" => Some(
            r#"
`@arguments` is a directive used to pass arguments to a fragment that was defined using `@argumentDefinitions`.

---
@see: https://relay.dev/docs/en/graphql-in-relay.html#arguments
"#,
        ),
        "uncheckedArguments_DEPRECATED" => Some(
            r#"
DEPRECATED version of `@arguments` directive.
`@arguments` is a directive used to pass arguments to a fragment that was defined using `@argumentDefinitions`.

---
@see: https://relay.dev/docs/en/graphql-in-relay.html#arguments
"#,
        ),
        _ => None,
    }.map(|s| MarkedString::String(s.to_string()))
}

pub fn on_hover(
    state: &impl GlobalState,
    params: <HoverRequest as Request>::Params,
) -> LSPRuntimeResult<<HoverRequest as Request>::Result> {
    let (document, position_span) =
        state.extract_executable_document_from_text(&params.text_document_position_params, 1)?;

    let resolution_path = document.resolve((), position_span);

    let project_name = state
        .extract_project_name_from_url(&params.text_document_position_params.text_document.uri)?;

    let schema = state.get_schema(&project_name)?;

    let schema_documentation = state.get_schema_documentation(project_name.lookup());

    Ok(get_hover(
        &resolution_path,
        &schema,
        project_name,
        &*state.get_extra_data_provider(),
        &schema_documentation,
        &state.get_program(&project_name)?,
        state.get_content_consumer_type(),
    ))
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GraphQLSchemaExplorerParams<'a> {
    path: Vec<&'a str>,

    schema_name: &'a str,

    #[serde(skip_serializing_if = "Option::is_none")]
    filter: Option<&'a str>,
}

fn get_open_schema_explorer_command_link(
    text: &str,
    params: &GraphQLSchemaExplorerParams<'_>,
) -> String {
    format!(
        "[{}](command:{})",
        text,
        get_open_schema_explorer_command(params)
    )
}

fn get_open_schema_explorer_command(params: &GraphQLSchemaExplorerParams<'_>) -> String {
    // see https://docs.rs/percent-encoding/2.1.0/percent_encoding/
    use percent_encoding::utf8_percent_encode;
    use percent_encoding::AsciiSet;
    use percent_encoding::CONTROLS;

    const FRAGMENT: AsciiSet = CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');

    return format!(
        "nuclide.relay-lsp.openSchemaExplorer?{}",
        utf8_percent_encode(&serde_json::to_string(params).unwrap(), &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 common::NamedItem;
use docblock_shared::RELAY_RESOLVER_DIRECTIVE_NAME;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::Program;
use graphql_ir::Value;
use graphql_syntax::FragmentDefinition;
use graphql_syntax::Identifier;
use graphql_syntax::OperationDefinition;
use graphql_syntax::VariableDefinition;
use graphql_text_printer::print_value;
use graphql_text_printer::PrinterOptions;
use intern::string_key::StringKey;
use intern::Lookup;
use lsp_types::Hover;
use lsp_types::HoverContents;
use lsp_types::MarkedString;
use resolution_path::ArgumentPath;
use resolution_path::ArgumentRoot;
use resolution_path::ConstantArgPath;
use resolution_path::ConstantBooleanPath;
use resolution_path::ConstantEnumPath;
use resolution_path::ConstantFloatPath;
use resolution_path::ConstantIntPath;
use resolution_path::ConstantListPath;
use resolution_path::ConstantNullPath;
use resolution_path::ConstantObjPath;
use resolution_path::ConstantObjectPath;
use resolution_path::ConstantStringPath;
use resolution_path::ConstantValueParent;
use resolution_path::ConstantValuePath;
use resolution_path::ConstantValueRoot;
use resolution_path::DefaultValuePath;
use resolution_path::DirectivePath;
use resolution_path::FragmentDefinitionPath;
use resolution_path::FragmentSpreadPath;
use resolution_path::IdentParent;
use resolution_path::IdentPath;
use resolution_path::InlineFragmentPath;
use resolution_path::LinkedFieldPath;
use resolution_path::ListTypeAnnotationPath;
use resolution_path::NamedTypeAnnotationPath;
use resolution_path::NonNullTypeAnnotationPath;
use resolution_path::OperationDefinitionPath;
use resolution_path::OperationPath;
use resolution_path::ResolutionPath;
use resolution_path::ScalarFieldPath;
use resolution_path::SelectionPath;
use resolution_path::TypeAnnotationPath;
use resolution_path::TypeConditionParent;
use resolution_path::TypeConditionPath;
use resolution_path::ValueListPath;
use resolution_path::ValuePath;
use resolution_path::VariableDefinitionPath;
use resolution_path::VariableIdentifierParent;
use resolution_path::VariableIdentifierPath;
use schema::SDLSchema;
use schema::Schema;
use schema_documentation::SchemaDocumentation;
use schema_print::print_directive;

use crate::hover::get_open_schema_explorer_command_link;
use crate::hover::GraphQLSchemaExplorerParams;
use crate::LSPExtraDataProvider;

/// Enum, that allows us to adjust content of the hover
/// tooltip based on the consumer type (Relay, GraphQL)
#[derive(Clone, Copy)]
pub enum ContentConsumerType {
    Relay,
    GraphQL,
}

impl ContentConsumerType {
    /// This method returns a rendered string (with encoded schema search params)
    fn render_text_with_params(
        &self,
        text: &str,
        params: &GraphQLSchemaExplorerParams<'_>,
    ) -> String {
        match self {
            ContentConsumerType::Relay => get_open_schema_explorer_command_link(text, params),
            ContentConsumerType::GraphQL => text.to_string(),
        }
    }
}

pub fn get_hover<'a>(
    path: &'a ResolutionPath<'a>,
    schema: &SDLSchema,
    schema_name: StringKey,
    extra_data_provider: &dyn LSPExtraDataProvider,
    schema_documentation: &impl SchemaDocumentation,
    program: &Program,
    content_consumer_type: ContentConsumerType,
) -> Option<Hover> {
    let hover_behavior = get_hover_behavior_from_resolution_path(path);
    let hover_content = get_hover_contents(
        hover_behavior,
        schema,
        schema_name,
        extra_data_provider,
        schema_documentation,
        program,
        content_consumer_type,
    );

    hover_content.map(|contents| Hover {
        contents,
        range: None,
    })
}

enum HoverBehavior<'a> {
    OperationDefinitionName(&'a OperationDefinition),
    OperationDefinitionRemainder,
    VariableDefinition(&'a VariableDefinition),
    VariableDefinitionList,
    ConstantValue(&'a ConstantValueParent<'a>),
    ScalarOrLinkedField(&'a Identifier, &'a SelectionPath<'a>),
    ArgumentPath(&'a ArgumentPath<'a>),
    InlineFragment(&'a InlineFragmentPath<'a>),
    FragmentSpread(&'a FragmentSpreadPath<'a>),
    Directive(&'a DirectivePath<'a>),
    FragmentDefinition(&'a FragmentDefinition),
    ExecutableDocument,
}

fn get_hover_behavior_from_resolution_path<'a>(path: &'a ResolutionPath<'a>) -> HoverBehavior<'a> {
    match path {
        // Show query stats on the operation definition name
        // and "query/subscription/mutation" token
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent:
                IdentParent::OperationDefinitionName(OperationDefinitionPath {
                    inner: operation_definition,
                    parent: _,
                }),
        }) => HoverBehavior::OperationDefinitionName(operation_definition),
        ResolutionPath::Operation(OperationPath {
            inner: _,
            parent:
                OperationDefinitionPath {
                    inner: operation_definition,
                    parent: _,
                },
        }) => HoverBehavior::OperationDefinitionName(operation_definition),
        // Explicitly show no hover in other parts of the operation definition.
        // For example, the curly braces after the operation variables are included in
        // this match arm.
        ResolutionPath::OperationDefinition(_) => HoverBehavior::OperationDefinitionRemainder,

        // Variables definition for both operations and fragments (?):
        // Name, Type and Default Value => info about variable and link to types
        // Directives handled later
        ResolutionPath::VariableIdentifier(VariableIdentifierPath {
            inner: _,
            parent:
                VariableIdentifierParent::VariableDefinition(VariableDefinitionPath {
                    inner: variable_definition,
                    parent: _,
                }),
        }) => HoverBehavior::VariableDefinition(variable_definition),
        ResolutionPath::DefaultValue(DefaultValuePath {
            inner: _,
            parent:
                VariableDefinitionPath {
                    inner: variable_definition,
                    parent: _,
                },
        }) => HoverBehavior::VariableDefinition(variable_definition),
        ResolutionPath::VariableDefinition(VariableDefinitionPath {
            inner: variable_definition,
            parent: _,
        }) => HoverBehavior::VariableDefinition(variable_definition),
        ResolutionPath::NonNullTypeAnnotation(NonNullTypeAnnotationPath {
            inner: _,
            parent: non_null_annotation_parent,
        }) => HoverBehavior::VariableDefinition(
            non_null_annotation_parent
                .parent
                .find_variable_definition_path()
                .inner,
        ),
        ResolutionPath::ListTypeAnnotation(ListTypeAnnotationPath {
            inner: _,
            parent: list_type_annotation_parent,
        }) => HoverBehavior::VariableDefinition(
            list_type_annotation_parent
                .parent
                .find_variable_definition_path()
                .inner,
        ),
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent:
                IdentParent::NamedTypeAnnotation(NamedTypeAnnotationPath {
                    inner: _,
                    parent:
                        TypeAnnotationPath {
                            inner: _,
                            parent: type_annotation_parent,
                        },
                }),
        }) => HoverBehavior::VariableDefinition(
            type_annotation_parent.find_variable_definition_path().inner,
        ),

        // Explicitly don't show hovers for VariableDefinitionList
        ResolutionPath::VariableDefinitionList(_) => HoverBehavior::VariableDefinitionList,

        // Constant values can either be rooted in variable definitions or arguments to
        // directives or fields. Handle those cases.
        ResolutionPath::ConstantInt(ConstantIntPath {
            inner: _,
            parent:
                ConstantValuePath {
                    inner: _,
                    parent: constant_value_parent,
                },
        }) => HoverBehavior::ConstantValue(constant_value_parent),
        ResolutionPath::ConstantFloat(ConstantFloatPath {
            inner: _,
            parent:
                ConstantValuePath {
                    inner: _,
                    parent: constant_value_parent,
                },
        }) => HoverBehavior::ConstantValue(constant_value_parent),
        ResolutionPath::ConstantString(ConstantStringPath {
            inner: _,
            parent:
                ConstantValuePath {
                    inner: _,
                    parent: constant_value_parent,
                },
        }) => HoverBehavior::ConstantValue(constant_value_parent),
        ResolutionPath::ConstantBoolean(ConstantBooleanPath {
            inner: _,
            parent:
                ConstantValuePath {
                    inner: _,
                    parent: constant_value_parent,
                },
        }) => HoverBehavior::ConstantValue(constant_value_parent),
        ResolutionPath::ConstantNull(ConstantNullPath {
            inner: _,
            parent:
                ConstantValuePath {
                    inner: _,
                    parent: constant_value_parent,
                },
        }) => HoverBehavior::ConstantValue(constant_value_parent),
        ResolutionPath::ConstantEnum(ConstantEnumPath {
            inner: _,
            parent:
                ConstantValuePath {
                    inner: _,
                    parent: constant_value_parent,
                },
        }) => HoverBehavior::ConstantValue(constant_value_parent),
        ResolutionPath::ConstantList(ConstantListPath {
            inner: _,
            parent: constant_value_path,
        }) => HoverBehavior::ConstantValue(&constant_value_path.parent),
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent:
                IdentParent::ConstantArgKey(ConstantArgPath {
                    inner: _,
                    parent:
                        ConstantObjPath {
                            inner: _,
                            parent: constant_value_path,
                        },
                }),
        }) => HoverBehavior::ConstantValue(&constant_value_path.parent),
        ResolutionPath::ConstantObj(ConstantObjPath {
            inner: _,
            parent: constant_value_path,
        }) => HoverBehavior::ConstantValue(&constant_value_path.parent),
        ResolutionPath::ConstantArg(ConstantArgPath {
            inner: _,
            parent: constant_obj_path,
        }) => HoverBehavior::ConstantValue(&constant_obj_path.parent.parent),

        // Scalar and linked fields
        ResolutionPath::ScalarField(ScalarFieldPath {
            inner: scalar_field,
            parent: selection_path,
        }) => HoverBehavior::ScalarOrLinkedField(&scalar_field.name, selection_path),
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent:
                IdentParent::ScalarFieldAlias(ScalarFieldPath {
                    inner: scalar_field,
                    parent: selection_path,
                }),
        }) => HoverBehavior::ScalarOrLinkedField(&scalar_field.name, selection_path),
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent:
                IdentParent::ScalarFieldName(ScalarFieldPath {
                    inner: scalar_field,
                    parent: selection_path,
                }),
        }) => HoverBehavior::ScalarOrLinkedField(&scalar_field.name, selection_path),
        ResolutionPath::LinkedField(LinkedFieldPath {
            inner: scalar_field,
            parent: selection_path,
        }) => HoverBehavior::ScalarOrLinkedField(&scalar_field.name, selection_path),
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent:
                IdentParent::LinkedFieldAlias(LinkedFieldPath {
                    inner: scalar_field,
                    parent: selection_path,
                }),
        }) => HoverBehavior::ScalarOrLinkedField(&scalar_field.name, selection_path),
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent:
                IdentParent::LinkedFieldName(LinkedFieldPath {
                    inner: scalar_field,
                    parent: selection_path,
                }),
        }) => HoverBehavior::ScalarOrLinkedField(&scalar_field.name, selection_path),

        // Field and directive arguments
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent: IdentParent::ArgumentValue(argument_path),
        }) => HoverBehavior::ArgumentPath(argument_path),
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent: IdentParent::ArgumentName(argument_path),
        }) => HoverBehavior::ArgumentPath(argument_path),
        ResolutionPath::VariableIdentifier(VariableIdentifierPath {
            inner: _,
            parent:
                VariableIdentifierParent::Value(ValuePath {
                    inner: _,
                    parent: value_parent,
                }),
        }) => HoverBehavior::ArgumentPath(value_parent.find_enclosing_argument_path()),
        ResolutionPath::ValueList(ValueListPath {
            inner: _,
            parent: value_path,
        }) => HoverBehavior::ArgumentPath(value_path.parent.find_enclosing_argument_path()),
        ResolutionPath::ConstantObject(ConstantObjectPath {
            inner: _,
            parent: value_path,
        }) => HoverBehavior::ArgumentPath(value_path.parent.find_enclosing_argument_path()),
        ResolutionPath::Argument(argument_path) => HoverBehavior::ArgumentPath(argument_path),

        // inline fragments
        ResolutionPath::InlineFragment(inline_fragment_path) => {
            HoverBehavior::InlineFragment(inline_fragment_path)
        }
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent:
                IdentParent::TypeConditionType(TypeConditionPath {
                    inner: _,
                    parent: TypeConditionParent::InlineFragment(inline_fragment_path),
                }),
        }) => HoverBehavior::InlineFragment(inline_fragment_path),
        ResolutionPath::TypeCondition(TypeConditionPath {
            inner: _,
            parent: TypeConditionParent::InlineFragment(inline_fragment_path),
        }) => HoverBehavior::InlineFragment(inline_fragment_path),

        // Fragment spreads
        ResolutionPath::FragmentSpread(fragment_spread_path) => {
            HoverBehavior::FragmentSpread(fragment_spread_path)
        }
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent: IdentParent::FragmentSpreadName(fragment_spread_path),
        }) => HoverBehavior::FragmentSpread(fragment_spread_path),

        ResolutionPath::Directive(directive_path) => HoverBehavior::Directive(directive_path),
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent: IdentParent::DirectiveName(directive_path),
        }) => HoverBehavior::Directive(directive_path),

        ResolutionPath::FragmentDefinition(FragmentDefinitionPath {
            inner: fragment_definition,
            parent: _,
        }) => HoverBehavior::FragmentDefinition(fragment_definition),
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent:
                IdentParent::FragmentDefinitionName(FragmentDefinitionPath {
                    inner: fragment_definition,
                    parent: _,
                }),
        }) => HoverBehavior::FragmentDefinition(fragment_definition),
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent:
                IdentParent::TypeConditionType(TypeConditionPath {
                    inner: _,
                    parent:
                        TypeConditionParent::FragmentDefinition(FragmentDefinitionPath {
                            inner: fragment_definition,
                            parent: _,
                        }),
                }),
        }) => HoverBehavior::FragmentDefinition(fragment_definition),
        ResolutionPath::TypeCondition(TypeConditionPath {
            inner: _,
            parent:
                TypeConditionParent::FragmentDefinition(FragmentDefinitionPath {
                    inner: fragment_definition,
                    parent: _,
                }),
        }) => HoverBehavior::FragmentDefinition(fragment_definition),

        // Explicitly show no hover content of operation/fragment definitions
        ResolutionPath::ExecutableDocument(_) => HoverBehavior::ExecutableDocument,
    }
}

fn get_hover_contents<'a>(
    hover_behavior: HoverBehavior<'a>,
    schema: &SDLSchema,
    schema_name: StringKey,
    extra_data_provider: &dyn LSPExtraDataProvider,
    schema_documentation: &impl SchemaDocumentation,
    program: &Program,
    content_consumer_type: ContentConsumerType,
) -> Option<HoverContents> {
    match hover_behavior {
        HoverBehavior::OperationDefinitionName(operation_definition) => {
            on_hover_operation(operation_definition, extra_data_provider)
        }
        HoverBehavior::OperationDefinitionRemainder => None,
        HoverBehavior::VariableDefinition(variable_definition) => Some(
            on_hover_variable_definition(variable_definition, schema_name, content_consumer_type),
        ),
        HoverBehavior::VariableDefinitionList => None,
        HoverBehavior::ConstantValue(constant_value_parent) => on_hover_constant_value(
            constant_value_parent,
            schema,
            schema_name,
            schema_documentation,
            content_consumer_type,
        ),
        HoverBehavior::ScalarOrLinkedField(field_name, selection_path) => {
            on_hover_scalar_or_linked_field(
                field_name,
                selection_path,
                schema,
                schema_name,
                schema_documentation,
                content_consumer_type,
            )
        }
        HoverBehavior::ArgumentPath(argument_path) => on_hover_argument_path(
            argument_path,
            schema,
            schema_name,
            schema_documentation,
            content_consumer_type,
        ),
        HoverBehavior::InlineFragment(inline_fragment_path) => on_hover_inline_fragment(
            inline_fragment_path,
            schema,
            schema_name,
            schema_documentation,
            content_consumer_type,
        ),
        HoverBehavior::FragmentSpread(fragment_spread_path) => on_hover_fragment_spread(
            fragment_spread_path,
            schema,
            schema_name,
            schema_documentation,
            program,
            content_consumer_type,
        ),
        HoverBehavior::Directive(directive_path) => on_hover_directive(directive_path, schema),
        HoverBehavior::FragmentDefinition(fragment_definition) => on_hover_fragment_definition(
            fragment_definition,
            schema,
            schema_name,
            schema_documentation,
            content_consumer_type,
        ),

        HoverBehavior::ExecutableDocument => None,
    }
}

fn on_hover_operation(
    operation_definition: &OperationDefinition,
    extra_data_provider: &dyn LSPExtraDataProvider,
) -> Option<HoverContents> {
    let name = operation_definition.name.as_ref()?;
    let extra_data = extra_data_provider.fetch_query_stats(name.value.lookup());
    if !extra_data.is_empty() {
        Some(HoverContents::Array(
            extra_data
                .iter()
                .map(|str| MarkedString::String(str.to_string()))
                .collect::<_>(),
        ))
    } else {
        None
    }
}

fn on_hover_variable_definition(
    variable_definition: &VariableDefinition,
    schema_name: StringKey,
    content_consumer_type: ContentConsumerType,
) -> HoverContents {
    let variable_identifier = &variable_definition.name;
    let variable_inner_type = variable_definition.type_.inner().name.value;
    let variable_type = &variable_definition.type_;
    let variable_default_value = variable_definition
        .default_value
        .as_ref()
        .map(|default_value| format!(" with default value `{}`", default_value.value))
        .unwrap_or_else(|| "".to_string());

    HoverContents::Scalar(MarkedString::String(format!(
        "`{}`: **{}**{}",
        variable_identifier,
        content_consumer_type.render_text_with_params(
            &variable_type.to_string(),
            &GraphQLSchemaExplorerParams {
                path: vec![variable_inner_type.lookup()],
                schema_name: schema_name.lookup(),
                filter: None,
            }
        ),
        variable_default_value
    )))
}

fn on_hover_constant_value<'a>(
    constant_value_parent: &'a ConstantValueParent<'a>,
    schema: &SDLSchema,
    schema_name: StringKey,
    schema_documentation: &impl SchemaDocumentation,
    content_consumer_type: ContentConsumerType,
) -> Option<HoverContents> {
    match constant_value_parent.find_constant_value_root() {
        ConstantValueRoot::VariableDefinition(variable_definition_path) => {
            Some(on_hover_variable_definition(
                variable_definition_path.inner,
                schema_name,
                content_consumer_type,
            ))
        }
        ConstantValueRoot::Argument(argument_path) => on_hover_argument_path(
            argument_path,
            schema,
            schema_name,
            schema_documentation,
            content_consumer_type,
        ),
    }
}

fn on_hover_argument_path<'a>(
    argument_path: &ArgumentPath<'a>,
    schema: &SDLSchema,
    schema_name: StringKey,
    schema_documentation: &impl SchemaDocumentation,
    content_consumer_type: ContentConsumerType,
) -> Option<HoverContents> {
    let ArgumentPath {
        inner: argument,
        parent,
    } = argument_path;

    let argument_name = argument.name.value.lookup();
    let argument_value = argument.value.to_string();
    let argument_info =
        MarkedString::String(format!("Argument `{}: {}`", argument_name, argument_value));

    let field_hover_info = match parent.find_argument_root() {
        ArgumentRoot::LinkedField(linked_field_path) => get_scalar_or_linked_field_hover_content(
            &linked_field_path.inner.name,
            &linked_field_path.parent,
            schema,
            schema_name,
            schema_documentation,
            content_consumer_type,
        ),
        ArgumentRoot::Directive(directive_path) => {
            get_directive_hover_content(directive_path, schema)
        }
        ArgumentRoot::ScalarField(scalar_field_path) => get_scalar_or_linked_field_hover_content(
            &scalar_field_path.inner.name,
            &scalar_field_path.parent,
            schema,
            schema_name,
            schema_documentation,
            content_consumer_type,
        ),
    }?;

    let mut contents = vec![argument_info];
    contents.extend(field_hover_info.into_iter());

    Some(HoverContents::Array(contents))
}

fn on_hover_scalar_or_linked_field(
    field_name: &Identifier,
    field_selection_path: &SelectionPath<'_>,
    schema: &SDLSchema,
    schema_name: StringKey,
    schema_documentation: &impl SchemaDocumentation,
    content_consumer_type: ContentConsumerType,
) -> Option<HoverContents> {
    let content = get_scalar_or_linked_field_hover_content(
        field_name,
        field_selection_path,
        schema,
        schema_name,
        schema_documentation,
        content_consumer_type,
    )?;
    Some(HoverContents::Array(content))
}

fn get_scalar_or_linked_field_hover_content(
    field_name: &Identifier,
    field_selection_path: &SelectionPath<'_>,
    schema: &SDLSchema,
    schema_name: StringKey,
    schema_documentation: &impl SchemaDocumentation,
    content_consumer_type: ContentConsumerType,
) -> Option<Vec<MarkedString>> {
    let parent_types = field_selection_path.parent.find_type_path(schema);
    let parent_type = parent_types.last()?;

    let mut type_path = parent_types
        .iter()
        .map(|parent_type| schema.get_type_name(*parent_type).lookup())
        .collect::<Vec<_>>();
    let parent_type_name = schema.get_type_name(*parent_type).lookup();

    let field = schema
        .named_field(*parent_type, field_name.value)
        .map(|id| schema.field(id))?;

    let is_resolver = field
        .directives
        .named(*RELAY_RESOLVER_DIRECTIVE_NAME)
        .is_some();

    let field_type_name = schema.get_type_name(field.type_.inner()).lookup();

    let mut hover_contents: Vec<MarkedString> = vec![MarkedString::String(format!(
        "Field: **{}**",
        field.name.item
    ))];

    if let Some(field_description) =
        schema_documentation.get_field_description(parent_type_name, field.name.item.lookup())
    {
        hover_contents.push(MarkedString::String(field_description.to_string()));
    } else if let Some(description) = field.description {
        hover_contents.push(MarkedString::String(description.to_string()));
    }

    type_path.push(field_type_name);

    let type_name = content_consumer_type.render_text_with_params(
        &schema.get_type_string(&field.type_),
        &GraphQLSchemaExplorerParams {
            path: type_path,
            schema_name: schema_name.lookup(),
            filter: None,
        },
    );

    if let Some(field_type_hack_source) = schema_documentation.get_hack_source(field_type_name) {
        hover_contents.push(MarkedString::String(format!(
            "Type: [**{}**]({})",
            type_name,
            codex_url_for_symbol(field_type_hack_source),
        )));
    } else {
        hover_contents.push(MarkedString::String(format!("Type: **{}**", type_name,)));
    }

    if let Some(type_description) = schema_documentation.get_type_description(field_type_name) {
        hover_contents.push(MarkedString::String(type_description.to_string()));
    }

    if !field.arguments.is_empty() {
        hover_contents.push(MarkedString::String(
            "This field accepts these arguments".to_string(),
        ));

        for arg in field.arguments.iter() {
            let arg_type_name = schema.get_type_name(arg.type_.inner()).lookup();
            hover_contents.push(MarkedString::from_markdown(format!(
                "{}: **{}**{}\n\n{}",
                arg.name,
                content_consumer_type.render_text_with_params(
                    &schema.get_type_string(&arg.type_),
                    &GraphQLSchemaExplorerParams {
                        path: vec![field_type_name, arg_type_name],
                        schema_name: schema_name.lookup(),
                        filter: None,
                    }
                ),
                if let Some(default_value) = &arg.default_value {
                    format!(" = {}", default_value)
                } else {
                    "".to_string()
                },
                if let Some(description) = schema_documentation.get_field_argument_description(
                    parent_type_name,
                    field.name.item.lookup(),
                    arg.name.0.lookup(),
                ) {
                    description.to_string()
                } else {
                    "".to_string()
                }
            )));
        }
    }

    if is_resolver {
        let msg = "**Relay Resolver**: This field is backed by a Relay Resolver, and is therefore only avaliable in Relay code. [Learn More](https://relay.dev/docs/next/guides/relay-resolvers/).";
        hover_contents.push(MarkedString::String(msg.to_string()))
    } else if field.is_extension {
        let msg = match content_consumer_type {
            ContentConsumerType::Relay => {
                "**Client Schema Extension**: This field was declared as a Relay Client Schema Extension, and is therefore only avalaible in Relay code. [Learn More](https://relay.dev/docs/guided-tour/updating-data/client-only-data/#client-only-data-client-schema-extensions)."
            }
            ContentConsumerType::GraphQL => {
                "**Client Schema Extension**: This field was declared as a GraphQL client schema extension explicitly among [these](https://fburl.com/code/9qg1gghd) files etc."
            }
        };
        hover_contents.push(MarkedString::String(msg.to_string()))
    }

    if let Some(field_hack_source) =
        schema_documentation.get_field_hack_source(parent_type_name, field.name.item.lookup())
    {
        hover_contents.push(MarkedString::String(format!(
            "View [**{}**]({}) in Codex",
            field_hack_source,
            codex_url_for_symbol(field_hack_source),
        )));
    }

    Some(hover_contents)
}

fn on_hover_inline_fragment(
    inline_fragment: &InlineFragmentPath<'_>,
    schema: &SDLSchema,
    schema_name: StringKey,
    schema_documentation: &impl SchemaDocumentation,
    content_consumer_type: ContentConsumerType,
) -> Option<HoverContents> {
    let InlineFragmentPath {
        inner: inline_fragment,
        parent: selection_path,
    } = inline_fragment;

    let type_condition = inline_fragment.type_condition.as_ref()?;
    let parent_types = selection_path.parent.find_type_path(schema);
    let parent_type = parent_types.last()?;

    let parent_type_name = schema.get_type_name(*parent_type);
    let parent_type_path = parent_types
        .iter()
        .map(|parent_type| schema.get_type_name(*parent_type).lookup())
        .collect::<Vec<_>>();

    let inline_fragment_condition = type_condition.type_.value.lookup();
    let inline_fragment_type_path = {
        let mut inline_fragment_type_path = parent_type_path.clone();
        inline_fragment_type_path.push(inline_fragment_condition);
        inline_fragment_type_path
    };

    let description = schema_documentation.get_type_description(inline_fragment_condition);

    let first_line = MarkedString::String(format!(
        "An inline fragment refining **{}** to **{}**",
        content_consumer_type.render_text_with_params(
            parent_type_name.lookup(),
            &GraphQLSchemaExplorerParams {
                path: parent_type_path,
                schema_name: schema_name.lookup(),
                filter: None,
            },
        ),
        content_consumer_type.render_text_with_params(
            inline_fragment_condition,
            &GraphQLSchemaExplorerParams {
                path: inline_fragment_type_path,
                schema_name: schema_name.lookup(),
                filter: None,
            },
        )
    ));

    let mut hover_contents: Vec<MarkedString> = vec![first_line];

    if let Some(description) = description {
        hover_contents.push(MarkedString::String(description.to_string()));
    }

    if let Some(hack_source) = schema_documentation.get_hack_source(inline_fragment_condition) {
        let codex_link = MarkedString::String(format!(
            "View [**{}**]({}) in Codex",
            hack_source,
            codex_url_for_symbol(hack_source),
        ));
        hover_contents.push(codex_link);
    }

    Some(HoverContents::Array(hover_contents))
}

fn on_hover_fragment_spread<'a>(
    fragment_spread_path: &'a FragmentSpreadPath<'a>,
    schema: &SDLSchema,
    schema_name: StringKey,
    schema_documentation: &impl SchemaDocumentation,
    program: &Program,
    content_consumer_type: ContentConsumerType,
) -> Option<HoverContents> {
    // TODO eventually show information about whether the fragment spread is
    // infallible, fallible, interface-on-interface, etc.

    let mut hover_contents = vec![];

    let FragmentSpreadPath {
        inner: fragment_spread,
        parent: _,
    } = fragment_spread_path;
    let fragment_name = FragmentDefinitionName(fragment_spread.name.value);

    let fragment_definition = program.fragment(fragment_name)?;

    let fragment_type_name = schema
        .get_type_name(fragment_definition.type_condition)
        .lookup();

    let rendered_fragment_type_name = content_consumer_type.render_text_with_params(
        fragment_type_name,
        &GraphQLSchemaExplorerParams {
            path: vec![fragment_type_name],
            schema_name: schema_name.lookup(),
            filter: None,
        },
    );

    hover_contents.push(MarkedString::String(format!(
        "fragment {} on {}",
        fragment_spread.name.value.lookup(),
        rendered_fragment_type_name,
    )));

    if !fragment_definition.variable_definitions.is_empty() {
        let mut variables_string: Vec<String> =
            vec!["This fragment accepts these arguments:".to_string()];
        for var in &fragment_definition.variable_definitions {
            let default_value = match var.default_value.clone() {
                Some(default_value) => format!(
                    ", with a default value of {}",
                    print_value(
                        schema,
                        &Value::Constant(default_value.item),
                        PrinterOptions::default()
                    )
                ),
                None => "".to_string(),
            };
            variables_string.push(format!(
                "* {}: {}{}",
                var.name.item,
                get_open_schema_explorer_command_link(
                    &schema.get_type_string(&var.type_),
                    &GraphQLSchemaExplorerParams {
                        path: vec![schema.get_type_name(var.type_.inner()).lookup()],
                        schema_name: schema_name.lookup(),
                        filter: None,
                    }
                ),
                default_value,
            ));
        }
        hover_contents.push(MarkedString::from_markdown(variables_string.join("\n")))
    }

    if matches!(content_consumer_type, ContentConsumerType::Relay) {
        let fragment_name_details: Vec<&str> = fragment_name.0.lookup().split('_').collect();
        // We expect the fragment name to be `ComponentName_propName`
        if fragment_name_details.len() == 2 {
            hover_contents.push(MarkedString::from_markdown(format!(
                r#"
To consume this fragment spread,
pass it to the component where it was defined.

For example:
```js
    <{} {}={{data.{}}} />
```
"#,
                fragment_name_details[0], fragment_name_details[1], fragment_name_details[1],
            )));
        } // We may log an error (later), if that is not the case.

        hover_contents.push(MarkedString::String(
            "@see: https://relay.dev/docs/en/thinking-in-relay#data-masking".to_string(),
        ));
    }

    if let Some(type_description) = schema_documentation.get_type_description(fragment_type_name) {
        if let Some(hack_source) = schema_documentation.get_hack_source(fragment_type_name) {
            hover_contents.push(MarkedString::String(format!(
                "Type Condition: on [**{}**]({})",
                rendered_fragment_type_name,
                codex_url_for_symbol(hack_source),
            )));
        } else {
            hover_contents.push(MarkedString::String(format!(
                "Type Condition: on **{}**",
                rendered_fragment_type_name,
            )));
        }

        hover_contents.push(MarkedString::String(type_description.to_string()));
    }

    Some(HoverContents::Array(hover_contents))
}

fn on_hover_directive<'a>(
    directive_path: &DirectivePath<'a>,
    schema: &SDLSchema,
) -> Option<HoverContents> {
    let content = get_directive_hover_content(directive_path, schema)?;
    Some(HoverContents::Array(content))
}

fn get_directive_hover_content<'a>(
    directive_path: &DirectivePath<'a>,
    schema: &SDLSchema,
) -> Option<Vec<MarkedString>> {
    let DirectivePath {
        inner: directive,
        parent: _,
    } = directive_path;

    let directive_name = DirectiveName(directive.name.value);

    if let Some(argument_definition_hover_info) =
        super::argument_definition_hover_info(directive_name.0.lookup())
    {
        return Some(vec![argument_definition_hover_info]);
    }

    let schema_directive = schema.get_directive(directive_name)?;

    let directive_definition = print_directive(schema, schema_directive);
    let markdown_definition = super::graphql_marked_string(directive_definition);
    let mut hover_contents: Vec<MarkedString> = vec![markdown_definition];
    if let Some(description) = schema_directive.description {
        hover_contents.push(MarkedString::String(description.to_string()));
    }

    Some(hover_contents)
}

fn on_hover_fragment_definition(
    fragment_definition: &FragmentDefinition,
    schema: &SDLSchema,
    schema_name: StringKey,
    schema_documentation: &impl SchemaDocumentation,
    content_consumer_type: ContentConsumerType,
) -> Option<HoverContents> {
    let fragment_name = fragment_definition.name.value;
    let fragment_type_condition = fragment_definition.type_condition.type_.value;
    let fragment_type = schema.get_type(fragment_type_condition)?;

    let type_name = schema.get_type_name(fragment_type);

    let rendered_parent_type_name = content_consumer_type.render_text_with_params(
        type_name.lookup(),
        &GraphQLSchemaExplorerParams {
            path: vec![type_name.lookup()],
            schema_name: schema_name.lookup(),
            filter: None,
        },
    );

    let title = MarkedString::from_markdown(format!(
        "fragment {} on {}",
        fragment_name, rendered_parent_type_name
    ));

    let mut hover_contents: Vec<MarkedString> = vec![title];

    if matches!(content_consumer_type, ContentConsumerType::Relay) {
        hover_contents.push(MarkedString::String(
            r#"Fragments let you select fields,
    and then include them in queries where you need to.

    ---
    @see: https://graphql.org/learn/queries/#fragments
    "#
            .to_string(),
        ))
    };

    if let Some(type_description) = schema_documentation.get_type_description(type_name.lookup()) {
        if let Some(hack_source) = schema_documentation.get_hack_source(type_name.lookup()) {
            hover_contents.push(MarkedString::String(format!(
                "Type Condition: on [**{}**]({})",
                rendered_parent_type_name,
                codex_url_for_symbol(hack_source),
            )));
        } else {
            hover_contents.push(MarkedString::String(format!(
                "Type Condition: on **{}**",
                rendered_parent_type_name
            )));
        }

        hover_contents.push(MarkedString::String(type_description.to_string()));
    }

    Some(HoverContents::Array(hover_contents))
}

fn codex_url_for_symbol(symbol: &str) -> String {
    // sanitize the symbol first by replacing instances of "::" with "/" to avoid breaking codex links
    let sanitized_symbol = str::replace(symbol, "::", "/");
    format!("https://www.internalfb.com/code/symbol/www/php/{sanitized_symbol}")
}


/*
 * 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::Span;
use graphql_syntax::Argument;
use graphql_syntax::Directive;
use graphql_syntax::ExecutableDefinition;
use graphql_syntax::ExecutableDocument;
use graphql_syntax::FragmentDefinition;
use graphql_syntax::FragmentSpread;
use graphql_syntax::InlineFragment;
use graphql_syntax::LinkedField;
use graphql_syntax::List;
use graphql_syntax::OperationDefinition;
use graphql_syntax::ScalarField;
use graphql_syntax::Selection;
use graphql_syntax::TypeCondition;
use intern::string_key::StringKey;

use crate::lsp_runtime_error::LSPRuntimeError;
use crate::lsp_runtime_error::LSPRuntimeResult;

mod type_path;
pub use type_path::TypePath;
pub use type_path::TypePathItem;

#[derive(Debug, Clone, PartialEq)]
pub enum NodeKind {
    OperationDefinition(OperationDefinition),
    FragmentDefinition(FragmentDefinition),
    FieldName,
    FieldArgument(StringKey, StringKey),
    FragmentSpread(StringKey),
    InlineFragment,
    Variable(String),
    Directive(StringKey, Option<StringKey>),
    TypeCondition(StringKey),
}

#[derive(Debug)]
pub struct NodeResolutionInfo {
    /// The type of the leaf node on which the information request was made
    pub kind: NodeKind,
    /// A list of type metadata that we can use to resolve the leaf
    /// type the request is being made against
    pub type_path: TypePath,
}

impl NodeResolutionInfo {
    fn new(kind: NodeKind) -> Self {
        Self {
            kind,
            type_path: Default::default(),
        }
    }
}

fn build_node_resolution_for_directive(
    directives: &[Directive],
    position_span: Span,
) -> Option<NodeResolutionInfo> {
    let directive = directives
        .iter()
        .find(|directive| directive.span.contains(position_span))?;

    let arg_name_opt = if let Some(args) = &directive.arguments {
        args.items
            .iter()
            .find(|arg| arg.span.contains(position_span))
            .map(|arg| arg.name.value)
    } else {
        None
    };

    Some(NodeResolutionInfo {
        kind: NodeKind::Directive(directive.name.value, arg_name_opt),
        type_path: Default::default(),
    })
}

fn type_condition_at_position(
    type_condition: &TypeCondition,
    position_span: Span,
) -> Option<NodeKind> {
    if !type_condition.span.contains(position_span) {
        return None;
    }

    Some(NodeKind::TypeCondition(type_condition.type_.value))
}

pub fn create_node_resolution_info(
    document: ExecutableDocument,
    position_span: Span,
) -> LSPRuntimeResult<NodeResolutionInfo> {
    let definition = document
        .definitions
        .iter()
        .find(|definition| definition.location().contains(position_span))
        .ok_or(LSPRuntimeError::ExpectedError)?;

    match definition {
        ExecutableDefinition::Operation(operation) => {
            if operation.location.contains(position_span) {
                let mut node_resolution_info =
                    NodeResolutionInfo::new(NodeKind::OperationDefinition(operation.clone()));
                let OperationDefinition {
                    selections,
                    variable_definitions,
                    ..
                } = operation;

                if let Some(variable_definitions) = variable_definitions {
                    if let Some(variable) = variable_definitions
                        .items
                        .iter()
                        .find(|var| var.span.contains(position_span))
                    {
                        node_resolution_info.kind = NodeKind::Variable(variable.type_.to_string());
                        return Ok(node_resolution_info);
                    }
                }

                let (_, kind) = operation.operation.clone().ok_or_else(|| {
                    LSPRuntimeError::UnexpectedError(
                        "Expected operation to exist, but it did not".to_string(),
                    )
                })?;
                node_resolution_info
                    .type_path
                    .add_type(TypePathItem::Operation(kind));

                build_node_resolution_info_from_selections(
                    selections,
                    position_span,
                    &mut node_resolution_info,
                );
                Ok(node_resolution_info)
            } else {
                Err(LSPRuntimeError::UnexpectedError(format!(
                    "Expected operation named {:?} to contain position {:?}, but it did not. Operation span {:?}",
                    operation.name, operation.location, position_span
                )))
            }
        }
        ExecutableDefinition::Fragment(fragment) => {
            if fragment.location.contains(position_span) {
                let mut node_resolution_info =
                    NodeResolutionInfo::new(NodeKind::FragmentDefinition(fragment.clone()));
                if let Some(node_resolution_info) =
                    build_node_resolution_for_directive(&fragment.directives, position_span)
                {
                    return Ok(node_resolution_info);
                }

                if let Some(node_kind) =
                    type_condition_at_position(&fragment.type_condition, position_span)
                {
                    node_resolution_info.kind = node_kind;
                    return Ok(node_resolution_info);
                }

                let type_name = fragment.type_condition.type_.value;
                node_resolution_info
                    .type_path
                    .add_type(TypePathItem::FragmentDefinition { type_name });
                build_node_resolution_info_from_selections(
                    &fragment.selections,
                    position_span,
                    &mut node_resolution_info,
                );
                Ok(node_resolution_info)
            } else {
                Err(LSPRuntimeError::UnexpectedError(format!(
                    "Expected fragment named {:?} to contain position {:?}, but it did not. Operation span {:?}",
                    fragment.name, fragment.location, position_span
                )))
            }
        }
    }
}

/// If position_span falls into one of the field arguments,
/// we need to display resolution info for this field
fn build_node_resolution_info_for_argument(
    field_name: StringKey,
    arguments: &Option<List<Argument>>,
    position_span: Span,
    node_resolution_info: &mut NodeResolutionInfo,
) -> Option<()> {
    if let Some(arguments) = &arguments {
        let argument = arguments
            .items
            .iter()
            .find(|item| item.span.contains(position_span))?;

        node_resolution_info.kind = NodeKind::FieldArgument(field_name, argument.name.value);

        Some(())
    } else {
        None
    }
}

fn build_node_resolution_info_from_selections(
    selections: &List<Selection>,
    position_span: Span,
    node_resolution_info: &mut NodeResolutionInfo,
) {
    if let Some(item) = selections
        .items
        .iter()
        .find(|item| item.span().contains(position_span))
    {
        if let Some(directive_resolution_info) =
            build_node_resolution_for_directive(item.directives(), position_span)
        {
            node_resolution_info.kind = directive_resolution_info.kind;
            return;
        }

        match item {
            Selection::LinkedField(node) => {
                node_resolution_info.kind = NodeKind::FieldName;
                let LinkedField {
                    name, selections, ..
                } = node;
                if build_node_resolution_info_for_argument(
                    name.value,
                    &node.arguments,
                    position_span,
                    node_resolution_info,
                )
                .is_none()
                {
                    node_resolution_info
                        .type_path
                        .add_type(TypePathItem::LinkedField { name: name.value });
                    build_node_resolution_info_from_selections(
                        selections,
                        position_span,
                        node_resolution_info,
                    );
                }
            }
            Selection::FragmentSpread(spread) => {
                let FragmentSpread { name, .. } = spread;
                if name.span.contains(position_span) {
                    node_resolution_info.kind = NodeKind::FragmentSpread(name.value);
                }
            }
            Selection::InlineFragment(node) => {
                let InlineFragment {
                    selections,
                    type_condition,
                    ..
                } = node;

                node_resolution_info.kind = NodeKind::InlineFragment;
                if let Some(type_condition) = type_condition {
                    let type_name = type_condition.type_.value;
                    node_resolution_info
                        .type_path
                        .add_type(TypePathItem::InlineFragment { type_name });

                    if let Some(node_kind) =
                        type_condition_at_position(type_condition, position_span)
                    {
                        node_resolution_info.kind = node_kind;
                    } else {
                        build_node_resolution_info_from_selections(
                            selections,
                            position_span,
                            node_resolution_info,
                        )
                    }
                }
            }
            Selection::ScalarField(node) => {
                let ScalarField { name, .. } = node;

                if build_node_resolution_info_for_argument(
                    name.value,
                    &node.arguments,
                    position_span,
                    node_resolution_info,
                )
                .is_none()
                {
                    node_resolution_info.kind = NodeKind::FieldName;
                    node_resolution_info
                        .type_path
                        .add_type(TypePathItem::ScalarField { name: name.value });
                }
            }
        }
    }
}

#[cfg(test)]
mod test {
    use common::SourceLocationKey;
    use common::Span;
    use graphql_syntax::parse_executable;
    use intern::string_key::Intern;

    use super::create_node_resolution_info;
    use super::NodeKind;
    use super::NodeResolutionInfo;

    fn parse_and_get_node_info(source: &str, pos: u32) -> NodeResolutionInfo {
        let document =
            parse_executable(source, SourceLocationKey::standalone("/test/file")).unwrap();

        // Select the `uri` field
        let position_span = Span {
            start: pos,
            end: pos,
        };

        create_node_resolution_info(document, position_span).unwrap()
    }

    #[test]
    fn create_node_resolution_info_test() {
        let node_resolution_info = parse_and_get_node_info(
            r#"
            fragment User_data on User {
                name
                profile_picture {
                    uri
                }
            }
        "#,
            // Select the `uri` field
            117,
        );

        assert_eq!(node_resolution_info.kind, NodeKind::FieldName);
    }

    #[test]
    fn create_node_resolution_info_test_position_outside() {
        let document = parse_executable(
            r#"
            fragment User_data on User {
                name
            }
        "#,
            SourceLocationKey::standalone("/test/file"),
        )
        .unwrap();
        // Position is outside of the document
        let position_span = Span { start: 86, end: 87 };
        let result = create_node_resolution_info(document, position_span);
        assert!(result.is_err());
    }

    #[test]
    fn create_node_resolution_info_fragment_def_name() {
        let node_resolution_info = parse_and_get_node_info(
            r#"
            fragment User_data on User {
                name
            }
        "#,
            // Select the `User_data` fragment name
            26,
        );

        match node_resolution_info.kind {
            NodeKind::FragmentDefinition(fragment) => {
                assert_eq!(fragment.name.value, "User_data".intern())
            }
            node_kind => panic!("Unexpected node node_resolution_info.kind {:?}", node_kind),
        }
    }

    #[test]
    fn create_node_resolution_info_fragment_def_type_condition() {
        let node_resolution_info = parse_and_get_node_info(
            r#"
            fragment User_data on User {
                name
            }
        "#,
            // Select the `User` type in fragment declaration
            35,
        );

        assert_eq!(
            node_resolution_info.kind,
            NodeKind::TypeCondition("User".intern())
        );
    }

    #[test]
    fn create_node_resolution_info_inline_fragment_type_condition() {
        let node_resolution_info = parse_and_get_node_info(
            r#"
            fragment User_data on User {
                name
                ... on User {
                    id
                }
            }
        "#,
            // Select the `User` type in fragment declaration
            84,
        );

        assert_eq!(
            node_resolution_info.kind,
            NodeKind::TypeCondition("User".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 graphql_syntax::OperationKind;
use intern::string_key::StringKey;
use schema::Field;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;

#[derive(Debug, Clone)]
/// An item in the list of type metadata that we can use to resolve the leaf
/// type the request (completion/hover) is being made against
pub enum TypePathItem {
    Operation(OperationKind),
    FragmentDefinition { type_name: StringKey },
    InlineFragment { type_name: StringKey },
    LinkedField { name: StringKey },
    ScalarField { name: StringKey },
}

/// Given a root path item and the schema this function will return a root type of the document
/// For operations -> Query/Mutation/Subscription
/// For fragments -> type of the fragment
fn resolve_root_type(root_path_item: TypePathItem, schema: &SDLSchema) -> Option<Type> {
    match root_path_item {
        TypePathItem::Operation(kind) => match kind {
            OperationKind::Query => schema.query_type(),
            OperationKind::Mutation => schema.mutation_type(),
            OperationKind::Subscription => schema.subscription_type(),
        },
        TypePathItem::FragmentDefinition { type_name } => schema.get_type(type_name),
        _ => {
            // TODO(brandondail) log here
            None
        }
    }
}

fn resolve_relative_type(
    parent_type: Type,
    path_item: TypePathItem,
    schema: &SDLSchema,
) -> Option<Type> {
    match path_item {
        TypePathItem::Operation(_) => {
            // TODO(brandondail) log here
            None
        }
        TypePathItem::FragmentDefinition { .. } => {
            // TODO(brandondail) log here
            None
        }
        TypePathItem::LinkedField { name } => {
            let field_id = schema.named_field(parent_type, name)?;
            let field = schema.field(field_id);
            Some(field.type_.inner())
        }
        TypePathItem::ScalarField { .. } => Some(parent_type),
        TypePathItem::InlineFragment { type_name } => schema.get_type(type_name),
    }
}

#[derive(Debug, Default)]
pub struct TypePath(pub Vec<TypePathItem>);

impl From<Vec<TypePathItem>> for TypePath {
    fn from(type_path: Vec<TypePathItem>) -> TypePath {
        TypePath(type_path)
    }
}

impl TypePath {
    pub fn add_type(&mut self, type_path_item: TypePathItem) {
        self.0.push(type_path_item)
    }

    /// Returns the leaf type, which is the type that the completion request is being made against.
    pub fn resolve_leaf_type(self, schema: &SDLSchema) -> Option<Type> {
        let mut type_path = self.0;
        type_path.reverse();
        let mut type_ =
            resolve_root_type(type_path.pop().expect("path must be non-empty"), schema)?;
        while let Some(path_item) = type_path.pop() {
            type_ = resolve_relative_type(type_, path_item, schema)?;
        }
        Some(type_)
    }

    /// Returns the leaf is it is a field
    pub fn resolve_current_field(self, schema: &SDLSchema) -> Option<(Type, &Field)> {
        let mut type_path = self.0;
        type_path.reverse();
        let mut type_ =
            resolve_root_type(type_path.pop().expect("path must be non-empty"), schema)?;
        while let Some(path_item) = type_path.pop() {
            if type_path.is_empty() {
                return match path_item {
                    TypePathItem::LinkedField { name } | TypePathItem::ScalarField { name } => {
                        schema
                            .named_field(type_, name)
                            .map(|field_id| (type_, schema.field(field_id)))
                    }
                    _ => None,
                };
            } else {
                type_ = resolve_relative_type(type_, path_item, schema)?;
            }
        }
        None
    }
}


/*
 * 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.
 */

//! Utilities for providing the goto definition feature

use std::path::Path;

use common::Location as IRLocation;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::FragmentSpread;
use graphql_ir::Program;
use graphql_ir::Visitor;
use intern::string_key::StringKey;
use lsp_types::request::References;
use lsp_types::request::Request;
use lsp_types::Location as LSPLocation;
use relay_docblock::DocblockIr;
use relay_docblock::On;
use schema::Schema;

use crate::docblock_resolution_info::DocblockResolutionInfo;
use crate::find_field_usages::find_field_locations;
use crate::find_field_usages::get_usages;
use crate::location::transform_relay_location_to_lsp_location;
use crate::lsp_runtime_error::LSPRuntimeError;
use crate::lsp_runtime_error::LSPRuntimeResult;
use crate::node_resolution_info::NodeKind;
use crate::server::GlobalState;
use crate::FeatureResolutionInfo;

fn get_references_response(
    feature_resolution_info: FeatureResolutionInfo,
    program: &Program,
    root_dir: &Path,
) -> LSPRuntimeResult<Vec<LSPLocation>> {
    match feature_resolution_info {
        FeatureResolutionInfo::GraphqlNode(node_resolution_info) => {
            match node_resolution_info.kind {
                NodeKind::FragmentDefinition(fragment) => {
                    let references =
                        ReferenceFinder::get_references_to_fragment(program, fragment.name.value)
                            .into_iter()
                            .map(|location| {
                                transform_relay_location_to_lsp_location(root_dir, location)
                            })
                            .collect::<Result<Vec<_>, LSPRuntimeError>>()?;

                    Ok(references)
                }
                NodeKind::FieldName => {
                    let (type_, field) = node_resolution_info
                        .type_path
                        .resolve_current_field(&program.schema)
                        .ok_or_else(|| {
                            LSPRuntimeError::UnexpectedError(" field not found!".to_string())
                        })?;
                    let type_name = program.schema.get_type_name(type_);
                    let field_name = field.name.item;

                    let lsp_locations =
                        get_usages(program, &program.schema, type_name, field_name)?
                            .into_iter()
                            .map(|(_, ir_location)| {
                                transform_relay_location_to_lsp_location(root_dir, ir_location)
                            })
                            .collect::<Result<Vec<_>, LSPRuntimeError>>()?;
                    Ok(lsp_locations)
                }
                _ => Err(LSPRuntimeError::ExpectedError),
            }
        }
        FeatureResolutionInfo::DocblockNode(docblock_node) => {
            if let DocblockResolutionInfo::FieldName(field_name) = docblock_node.resolution_info {
                let type_name = match docblock_node.ir {
                    DocblockIr::RelayResolver(relay_resolver) => match relay_resolver.on {
                        On::Type(type_) => type_.value.item,
                        On::Interface(interface) => interface.value.item,
                    },
                    DocblockIr::TerseRelayResolver(_) => {
                        // TODO: Implement support for terse relay resolvers.
                        return Err(LSPRuntimeError::ExpectedError);
                    }
                    DocblockIr::StrongObjectResolver(_) => {
                        // TODO: Implement support for strong object.
                        return Err(LSPRuntimeError::ExpectedError);
                    }
                    DocblockIr::WeakObjectType(_) => {
                        // TODO: Implement support for weak object.
                        return Err(LSPRuntimeError::ExpectedError);
                    }
                };

                let references = find_field_locations(program, field_name, type_name)
                    .ok_or(LSPRuntimeError::ExpectedError)?
                    .into_iter()
                    .map(|location| transform_relay_location_to_lsp_location(root_dir, location))
                    .collect::<Result<Vec<_>, LSPRuntimeError>>()?;

                Ok(references)
            } else {
                // Go to reference not implemented for other parts of the docblocks yet.
                Err(LSPRuntimeError::ExpectedError)
            }
        }
    }
}

#[derive(Debug, Clone)]
struct ReferenceFinder {
    references: Vec<IRLocation>,
    name: StringKey,
}

impl ReferenceFinder {
    fn get_references_to_fragment(program: &Program, name: StringKey) -> Vec<IRLocation> {
        let mut reference_finder = ReferenceFinder {
            references: vec![],
            name,
        };
        reference_finder.visit_program(program);
        reference_finder.references
    }
}

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

    fn visit_fragment_spread(&mut self, spread: &FragmentSpread) {
        if spread.fragment.item == FragmentDefinitionName(self.name) {
            self.references.push(spread.fragment.location);
        }
    }
}

pub fn on_references(
    state: &impl GlobalState,
    params: <References as Request>::Params,
) -> LSPRuntimeResult<<References as Request>::Result> {
    let node_resolution_info = state.resolve_node(&params.text_document_position)?;
    let references_response = get_references_response(
        node_resolution_info,
        &state
            .get_program(&state.extract_project_name_from_url(
                &params.text_document_position.text_document.uri,
            )?)?,
        &state.root_dir(),
    )?;
    Ok(Some(references_response))
}


/*
 * 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 lsp_types::request::Request;
use lsp_types::Position;
use lsp_types::TextDocumentIdentifier;
use lsp_types::TextDocumentPositionParams;
use lsp_types::Url;
use schema::Schema;
use serde::Deserialize;
use serde::Serialize;

use crate::lsp_runtime_error::LSPRuntimeError;
use crate::lsp_runtime_error::LSPRuntimeResult;
use crate::server::GlobalState;
use crate::FeatureResolutionInfo;

pub(crate) enum ResolvedTypesAtLocation {}

#[derive(Deserialize, Serialize)]
pub(crate) struct ResolvedTypesAtLocationParams {
    file_name: String,
    line: u32,
    character: u32,
}

impl ResolvedTypesAtLocationParams {
    fn to_text_document_position_params(&self) -> LSPRuntimeResult<TextDocumentPositionParams> {
        Ok(TextDocumentPositionParams {
            text_document: TextDocumentIdentifier {
                uri: Url::parse(&self.file_name).map_err(|_| LSPRuntimeError::ExpectedError)?,
            },
            position: Position {
                character: self.character,
                line: self.line,
            },
        })
    }
}

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ResolvedTypesAtLocationResponse {
    pub path_and_schema_name: PathAndSchemaName,
}

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PathAndSchemaName {
    pub path: Vec<String>,
    pub schema_name: String,
}

impl Request for ResolvedTypesAtLocation {
    type Params = ResolvedTypesAtLocationParams;
    type Result = ResolvedTypesAtLocationResponse;
    const METHOD: &'static str = "relay/getResolvedTypesAtLocation";
}

pub(crate) fn on_get_resolved_types_at_location(
    state: &impl GlobalState,
    params: <ResolvedTypesAtLocation as Request>::Params,
) -> LSPRuntimeResult<<ResolvedTypesAtLocation as Request>::Result> {
    let params = params.to_text_document_position_params()?;
    let project_name = state.extract_project_name_from_url(&params.text_document.uri)?;
    let schema = state.get_schema(&project_name)?;
    let schema_name = project_name.to_string();

    if let Ok(feature_resolution_info) = state.resolve_node(&params) {
        match feature_resolution_info {
            FeatureResolutionInfo::GraphqlNode(node_resolution_info) => {
                // If type_path is empty, type_path.resolve_current_field() will panic.
                if !node_resolution_info.type_path.0.is_empty() {
                    let type_and_field = node_resolution_info
                        .type_path
                        .resolve_current_field(&schema);
                    if let Some((_parent_type, field)) = type_and_field {
                        let type_name = schema.get_type_name(field.type_.inner()).to_string();
                        // TODO resolve enclosing types, not just types immediately under the cursor
                        return Ok(ResolvedTypesAtLocationResponse {
                            path_and_schema_name: PathAndSchemaName {
                                path: vec![type_name],
                                schema_name,
                            },
                        });
                    }
                }
            }
            FeatureResolutionInfo::DocblockNode(_) => {
                // Get types at location not implemented for docblocks yet.
                return Ok(ResolvedTypesAtLocationResponse {
                    path_and_schema_name: PathAndSchemaName {
                        path: Vec::default(),
                        schema_name,
                    },
                });
            }
        }
    }

    Ok(ResolvedTypesAtLocationResponse {
        path_and_schema_name: PathAndSchemaName {
            path: Vec::default(),
            schema_name,
        },
    })
}


/*
 * 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::reexport::StringKey;
use intern::string_key::Intern;
use intern::Lookup;
use lsp_types::request::Request;
use schema::Schema;
use schema_documentation::SchemaDocumentation;
use serde::Deserialize;
use serde::Serialize;

use crate::lsp_runtime_error::LSPRuntimeResult;
use crate::server::GlobalState;

pub(crate) enum SearchSchemaItems {}

#[derive(Deserialize, Serialize)]
pub(crate) struct SchemaSearchItem {
    name: String,
    description: Option<String>,
}

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SearchSchemaItemsResponse {
    pub items: Vec<SchemaSearchItem>,
    pub has_more: bool,
}

#[derive(Deserialize, Serialize)]
pub(crate) struct SearchSchemaItemsParams {
    pub filter: Option<String>,
    pub schema_name: String,
    pub take: Option<usize>,
    pub skip: Option<usize>,
}

impl Request for SearchSchemaItems {
    type Params = SearchSchemaItemsParams;
    type Result = SearchSchemaItemsResponse;
    const METHOD: &'static str = "relay/searchSchemaItems";
}

pub(crate) fn on_search_schema_items(
    state: &impl GlobalState,
    params: SearchSchemaItemsParams,
) -> LSPRuntimeResult<<SearchSchemaItems as Request>::Result> {
    let filter = params.filter.map(|f| f.to_lowercase());
    let schema_name: &str = &params.schema_name;
    let schema = state.get_schema(&schema_name.intern())?;

    let schema_documentation = state.get_schema_documentation(schema_name);

    let objects = filter_and_transform_items(
        schema.objects().map(|o| o.name.item.0),
        &schema_documentation,
        &filter,
    );
    let interfaces = filter_and_transform_items(
        schema.interfaces().map(|i| i.name.item.0),
        &schema_documentation,
        &filter,
    );
    let enums = filter_and_transform_items(
        schema.enums().map(|e| e.name.item.0),
        &schema_documentation,
        &filter,
    );
    let unions = filter_and_transform_items(
        schema.unions().map(|u| u.name.item),
        &schema_documentation,
        &filter,
    );
    let input_objects = filter_and_transform_items(
        schema.input_objects().map(|io| io.name.item.0),
        &schema_documentation,
        &filter,
    );
    let scalars = filter_and_transform_items(
        schema.scalars().map(|s| s.name.item.0),
        &schema_documentation,
        &filter,
    );

    let mut items = objects
        .chain(interfaces)
        .chain(enums)
        .chain(unions)
        .chain(input_objects)
        .chain(scalars)
        .collect::<Vec<_>>();

    items.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));

    let skip = *params.skip.as_ref().unwrap_or(&0);
    let take = *params.take.as_ref().unwrap_or(&500);
    let has_more = items.len() > skip + take;

    let items = items.into_iter().skip(skip).take(take).collect::<Vec<_>>();

    Ok(SearchSchemaItemsResponse { items, has_more })
}

fn filter_and_transform_items<'a>(
    items: impl Iterator<Item = StringKey> + 'a,
    schema_documentation: &'a impl SchemaDocumentation,
    filter: &'a Option<String>,
) -> impl Iterator<Item = SchemaSearchItem> + 'a {
    items.filter_map(move |obj| {
        let name = obj.lookup();
        let description = schema_documentation
            .get_type_description(name)
            .map(|s| s.to_string());

        if should_include_named_item(name, &description, filter) {
            Some(SchemaSearchItem {
                name: name.to_string(),
                description,
            })
        } else {
            None
        }
    })
}

fn should_include_named_item(
    name: &str,
    description: &Option<String>,
    filter: &Option<String>,
) -> bool {
    if let Some(filter) = filter.as_ref() {
        if name.to_lowercase().contains(filter) {
            true
        } else if let Some(description) = description {
            description.to_lowercase().contains(filter)
        } else {
            false
        }
    } else {
        true
    }
}

#[cfg(test)]
mod tests {
    use crate::search_schema_items::should_include_named_item;

    #[test]
    fn test_no_filter() {
        assert_eq!(
            should_include_named_item("Yohan Blake", &Some("London 2012".to_string()), &None),
            true
        );
        assert_eq!(should_include_named_item("Usain Bolt", &None, &None), true);
    }

    #[test]
    fn test_filter_matches_name() {
        assert_eq!(
            should_include_named_item("Michael Frater", &None, &Some("michael".to_string())),
            true
        );
        assert_eq!(
            should_include_named_item(
                "Nesta Carter",
                &Some("London 2012".to_string()),
                &Some("nesta".to_string())
            ),
            true
        );
    }

    #[test]
    fn test_filter_matches_description() {
        assert_eq!(
            should_include_named_item(
                "Tiana Bartoletta",
                &Some("London 2012".to_string()),
                &Some("london".to_string())
            ),
            true
        );
    }

    #[test]
    fn test_non_matching_filter() {
        assert_eq!(
            should_include_named_item(
                "Allyson Felix",
                &Some("London 2012".to_string()),
                &Some("salt lake city".to_string())
            ),
            false
        );
    }
}