/*
 * 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 super::GlobalState;
use crate::LSPRuntimeResult;

pub(crate) struct HeartbeatRequest;

impl Request for HeartbeatRequest {
    type Params = ();
    type Result = String;
    const METHOD: &'static str = "relay/heartbeat";
}

#[allow(clippy::unnecessary_wraps)]
pub(crate) fn on_heartbeat(
    _state: &impl GlobalState,
    _params: <HeartbeatRequest as Request>::Params,
) -> LSPRuntimeResult<<HeartbeatRequest as Request>::Result> {
    Ok("Connected.".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::ops::ControlFlow;

use lsp_types::notification::Notification;

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

pub struct LSPNotificationDispatch<'state, TState> {
    notification: lsp_server::Notification,
    state: &'state TState,
}

impl<'state, TState> LSPNotificationDispatch<'state, TState> {
    pub fn new(notification: lsp_server::Notification, state: &'state TState) -> Self {
        LSPNotificationDispatch {
            notification,
            state,
        }
    }

    /// Calls handler if the LSPNotificationDispatch's notification's method matches
    /// the method of TNotification. Returns a ControlFlow which will be Break if the handler
    /// was called, or Continue otherwise.
    /// Thus, multiple calls to `on_notification_sync(...)?` can be chained. Doing so will
    /// cause LSPNotificationDispatch to execute the first matching handler, if any.
    pub fn on_notification_sync<TNotification: Notification>(
        self,
        handler: fn(&TState, TNotification::Params) -> LSPRuntimeResult<()>,
    ) -> ControlFlow<Option<LSPRuntimeError>, Self> {
        if self.notification.method == TNotification::METHOD {
            let params = extract_notification_params::<TNotification>(self.notification);
            // TODO propagate these errors
            let response = handler(self.state, params);

            return ControlFlow::Break(response.err());
        }

        ControlFlow::Continue(self)
    }

    pub fn notification(self) -> lsp_server::Notification {
        self.notification
    }
}

fn extract_notification_params<N>(notification: lsp_server::Notification) -> N::Params
where
    N: Notification,
{
    notification
        .extract(N::METHOD)
        .expect("extract_notification_params: could not extract notification param")
}

#[cfg(test)]
mod test {
    use std::ops::ControlFlow;
    use std::sync::atomic::AtomicI32;
    use std::sync::atomic::Ordering;

    use lsp_types::notification::LogMessage;
    use lsp_types::notification::Notification;
    use lsp_types::notification::TelemetryEvent;
    use lsp_types::LogMessageParams;
    use lsp_types::MessageType;

    use super::LSPNotificationDispatch;
    use crate::lsp_runtime_error::LSPRuntimeResult;

    #[test]
    fn calls_first_matching_notification_handler() {
        let state: AtomicI32 = AtomicI32::new(0);
        let dispatch = LSPNotificationDispatch::new(
            lsp_server::Notification {
                method: "window/logMessage".to_string(),
                params: serde_json::to_value(LogMessageParams {
                    typ: MessageType::ERROR,
                    message: "Use Relay!".to_string(),
                })
                .unwrap(),
            },
            &state,
        );
        let dispatch = || {
            dispatch
                .on_notification_sync::<TelemetryEvent>(telemetry_handler)?
                .on_notification_sync::<LogMessage>(log_message_handler)?;
            ControlFlow::Continue(())
        };

        let result = dispatch();
        assert!(result.is_break());
        assert_eq!(state.load(Ordering::Relaxed), 2);
    }

    fn telemetry_handler(
        state: &AtomicI32,
        _params: <TelemetryEvent as Notification>::Params,
    ) -> LSPRuntimeResult<()> {
        state.store(1, Ordering::Relaxed);

        Ok(())
    }

    fn log_message_handler(
        state: &AtomicI32,
        _params: <LogMessage as Notification>::Params,
    ) -> LSPRuntimeResult<()> {
        state.store(2, Ordering::Relaxed);

        Ok(())
    }
}


/*
 * 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::ops::ControlFlow;

use lsp_server::ErrorCode;
use lsp_server::Request as ServerRequest;
use lsp_server::RequestId;
use lsp_server::RequestId as ServerRequestId;
use lsp_server::Response as ServerResponse;
use lsp_server::ResponseError;
use lsp_types::request::Request;
use serde_json::Value;

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

pub struct LSPRequestDispatch<'state, TState> {
    request: lsp_server::Request,
    state: &'state TState,
}

impl<'state, TState> LSPRequestDispatch<'state, TState> {
    pub fn new(request: lsp_server::Request, state: &'state TState) -> Self {
        LSPRequestDispatch { request, state }
    }

    /// Calls handler if the LSPRequestDispatch's request's method matches the method
    /// of TRequest. Returns a ControlFlow which will be Break if the handler was called,
    /// or Continue otherwise.
    /// Thus, multiple calls to `on_request_sync(...)?` can be chained. Doing so will
    /// cause LSPRequestDispatch to execute the first matching handler, if any.
    pub fn on_request_sync<TRequest: Request>(
        self,
        handler: fn(&TState, TRequest::Params) -> LSPRuntimeResult<TRequest::Result>,
    ) -> ControlFlow<ServerResponse, Self> {
        if self.request.method == TRequest::METHOD {
            match extract_request_params::<TRequest>(self.request) {
                Ok((request_id, params)) => {
                    let response = handler(self.state, params).and_then(|handler_result| {
                        serde_json::to_value(handler_result).map_err(|_err| {
                            LSPRuntimeError::UnexpectedError(
                                "Unable to serialize request response".to_string(),
                            )
                        })
                    });
                    let server_response = convert_to_lsp_response(request_id, response);

                    return ControlFlow::Break(server_response);
                }
                Err(error) => {
                    return ControlFlow::Break(convert_to_lsp_response(
                        ServerRequestId::from("default-lsp-id".to_string()),
                        Err(error),
                    ));
                }
            }
        }

        ControlFlow::Continue(self)
    }

    pub fn request(self) -> lsp_server::Request {
        self.request
    }
}

fn convert_to_lsp_response(
    id: RequestId,
    result: LSPRuntimeResult<serde_json::Value>,
) -> ServerResponse {
    match result {
        Ok(result) => ServerResponse {
            id,
            result: Some(result),
            error: None,
        },
        Err(LSPRuntimeError::ExpectedError) => ServerResponse {
            id,
            result: Some(Value::Null),
            error: None,
        },
        Err(runtime_error) => {
            let response_error: Option<ResponseError> = runtime_error.into();
            let response_error = response_error.unwrap_or_else(|| ResponseError {
                code: ErrorCode::UnknownErrorCode as i32,
                message: "Request Canceled".to_string(),
                data: None,
            });
            ServerResponse {
                id,
                result: None,
                error: Some(response_error),
            }
        }
    }
}

fn extract_request_params<R>(req: ServerRequest) -> LSPRuntimeResult<(ServerRequestId, R::Params)>
where
    R: Request,
{
    std::panic::catch_unwind(|| {
        req.extract(R::METHOD)
            .expect("extract_request_params: could not extract request params")
    })
    .map_err(|err| {
        LSPRuntimeError::UnexpectedError(format!(
            "panic in the `extract_request_params`: {:?}",
            err
        ))
    })
}

#[cfg(test)]
mod test {
    use std::ops::ControlFlow;
    use std::sync::atomic::AtomicI32;
    use std::sync::atomic::Ordering;

    use lsp_types::request::GotoDefinition;
    use lsp_types::request::HoverRequest;
    use lsp_types::request::Request;
    use lsp_types::Position;
    use lsp_types::TextDocumentIdentifier;
    use lsp_types::TextDocumentPositionParams;
    use lsp_types::Url;

    use super::LSPRequestDispatch;
    use crate::lsp_runtime_error::LSPRuntimeResult;

    #[test]
    fn calls_first_matching_request_handler() {
        let state: AtomicI32 = AtomicI32::new(0);
        let dispatch = LSPRequestDispatch::new(
            lsp_server::Request {
                id: "id".to_string().into(),
                method: "textDocument/definition".to_string(),
                params: serde_json::to_value(TextDocumentPositionParams {
                    text_document: TextDocumentIdentifier {
                        uri: Url::parse("https://example.net").unwrap(),
                    },
                    position: Position {
                        line: 0,
                        character: 0,
                    },
                })
                .unwrap(),
            },
            &state,
        );
        let dispatch = || {
            dispatch
                .on_request_sync::<HoverRequest>(hover_handler)?
                .on_request_sync::<GotoDefinition>(goto_definition_handler)?;
            ControlFlow::Continue(())
        };
        let result = dispatch();
        assert!(result.is_break());
        assert_eq!(state.load(Ordering::Relaxed), 2);
    }

    fn hover_handler(
        state: &AtomicI32,
        _params: <HoverRequest as Request>::Params,
    ) -> LSPRuntimeResult<<HoverRequest as Request>::Result> {
        state.store(1, Ordering::Relaxed);

        Ok(None)
    }

    fn goto_definition_handler(
        state: &AtomicI32,
        _params: <GotoDefinition as Request>::Params,
    ) -> LSPRuntimeResult<<GotoDefinition as Request>::Result> {
        state.store(2, Ordering::Relaxed);

        Ok(None)
    }
}

#[cfg(test)]
mod tests {
    use lsp_types::request::CodeActionRequest;
    use serde_json::json;

    use super::*;

    #[test]
    fn test_extract_request_params_error() {
        let request_id = ServerRequestId::from("test-id".to_string());
        let request = ServerRequest {
            id: request_id,
            method: "textDocument/codeAction".to_string(),
            params: json!({
                "invalid_key": "invalid_value",
            }),
        };
        let result = extract_request_params::<CodeActionRequest>(request);
        // it returns an error, but not panic
        assert!(result.is_err());
    }
}


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

use common::PerfLogger;
use common::SourceLocationKey;
use common::Span;
use crossbeam::channel::SendError;
use crossbeam::channel::Sender;
use dashmap::mapref::entry::Entry;
use dashmap::DashMap;
use docblock_syntax::parse_docblock;
use extract_graphql::JavaScriptSourceFeature;
use fnv::FnvBuildHasher;
use graphql_ir::build_ir_with_extra_features;
use graphql_ir::BuilderOptions;
use graphql_ir::FragmentVariablesSemantic;
use graphql_ir::Program;
use graphql_ir::RelayMode;
use graphql_syntax::parse_executable_with_error_recovery;
use graphql_syntax::ExecutableDefinition;
use graphql_syntax::ExecutableDocument;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use log::debug;
use lsp_server::Message;
use lsp_types::Diagnostic;
use lsp_types::Range;
use lsp_types::TextDocumentPositionParams;
use lsp_types::Url;
use relay_compiler::config::Config;
use relay_compiler::FileCategorizer;
use relay_docblock::parse_docblock_ast;
use relay_docblock::ParseOptions;
use relay_transforms::deprecated_fields_for_executable_definition;
use schema::SDLSchema;
use schema_documentation::CombinedSchemaDocumentation;
use schema_documentation::SchemaDocumentation;
use schema_documentation::SchemaDocumentationLoader;
use tokio::sync::Notify;

use super::task_queue::TaskScheduler;
use crate::diagnostic_reporter::DiagnosticReporter;
use crate::docblock_resolution_info::create_docblock_resolution_info;
use crate::graphql_tools::get_query_text;
use crate::js_language_server::JSLanguageServer;
use crate::lsp_runtime_error::LSPRuntimeResult;
use crate::node_resolution_info::create_node_resolution_info;
use crate::utils::extract_executable_definitions_from_text_document;
use crate::utils::extract_feature_from_text;
use crate::utils::extract_project_name_from_url;
use crate::ContentConsumerType;
use crate::DocblockNode;
use crate::Feature;
use crate::FeatureResolutionInfo;
use crate::LSPExtraDataProvider;
use crate::LSPRuntimeError;

pub type Schemas = Arc<DashMap<StringKey, Arc<SDLSchema>, FnvBuildHasher>>;
pub type SourcePrograms = Arc<DashMap<StringKey, Program, FnvBuildHasher>>;
pub type ProjectStatusMap = Arc<DashMap<StringKey, ProjectStatus, FnvBuildHasher>>;

#[derive(Eq, PartialEq)]
pub enum ProjectStatus {
    Activated,
    Completed,
}

pub trait GlobalState {
    type TSchemaDocumentation: SchemaDocumentation;

    fn get_schema(&self, project_name: &StringKey) -> LSPRuntimeResult<Arc<SDLSchema>>;

    fn get_program(&self, project_name: &StringKey) -> LSPRuntimeResult<Program>;

    fn resolve_node(
        &self,
        text_document_position: &TextDocumentPositionParams,
    ) -> LSPRuntimeResult<FeatureResolutionInfo>;

    fn root_dir(&self) -> PathBuf;

    fn extract_executable_document_from_text(
        &self,
        position: &TextDocumentPositionParams,
        index_offset: usize,
    ) -> LSPRuntimeResult<(ExecutableDocument, Span)>;

    fn extract_feature_from_text(
        &self,
        position: &TextDocumentPositionParams,
        index_offset: usize,
    ) -> LSPRuntimeResult<(Feature, Span)>;

    fn get_schema_documentation(&self, schema_name: &str) -> Self::TSchemaDocumentation;

    fn get_extra_data_provider(&self) -> &dyn LSPExtraDataProvider;

    fn resolve_executable_definitions(
        &self,
        text_document_uri: &Url,
    ) -> LSPRuntimeResult<Vec<ExecutableDefinition>>;

    fn get_diagnostic_for_range(&self, url: &Url, range: Range) -> Option<Diagnostic>;

    /// For Relay - project_name is an human-readable identifier of a set of configurations,
    /// source files, schema extensions, etc, that are compiled together using a single GraphQL
    /// Schema. project_name typically the same as the schema name: facebook, intern, etc.
    /// For Native - it may be a BuildConfigName.
    fn extract_project_name_from_url(&self, url: &Url) -> LSPRuntimeResult<StringKey>;

    /// Experimental (Relay-only) JS Language Server instance
    fn get_js_language_sever(&self) -> Option<&dyn JSLanguageServer<TState = Self>>;

    /// This is powering the functionality of executing GraphQL query from the IDE
    fn get_full_query_text(
        &self,
        query_text: String,
        project_name: &StringKey,
    ) -> LSPRuntimeResult<String>;

    fn document_opened(&self, url: &Url, text: &str) -> LSPRuntimeResult<()>;

    fn document_changed(&self, url: &Url, full_text: &str) -> LSPRuntimeResult<()>;

    fn document_closed(&self, url: &Url) -> LSPRuntimeResult<()>;

    /// To distinguish content, that we show to consumers
    /// we may need to know who's our current consumer.
    /// This is mostly for hover handler (where we render markup)
    fn get_content_consumer_type(&self) -> ContentConsumerType;
}

/// This structure contains all available resources that we may use in the Relay LSP message/notification
/// handlers. Such as schema, programs, extra_data_providers, etc...
pub struct LSPState<
    TPerfLogger: PerfLogger + 'static,
    TSchemaDocumentation: SchemaDocumentation + 'static,
> {
    pub(crate) config: Arc<Config>,
    pub(crate) sender: Sender<Message>,
    task_scheduler: Arc<TaskScheduler<super::Task>>,
    root_dir: PathBuf,
    extra_data_provider: Box<dyn LSPExtraDataProvider>,
    file_categorizer: FileCategorizer,
    pub(crate) schemas: Schemas,
    schema_documentation_loader: Option<Box<dyn SchemaDocumentationLoader<TSchemaDocumentation>>>,
    pub(crate) source_programs: SourcePrograms,
    synced_javascript_features: DashMap<Url, Vec<JavaScriptSourceFeature>>,
    pub(crate) perf_logger: Arc<TPerfLogger>,
    pub(crate) diagnostic_reporter: Arc<DiagnosticReporter>,
    pub(crate) notify_lsp_state_resources: Arc<Notify>,
    pub(crate) project_status: ProjectStatusMap,
    js_resource: Option<Box<dyn JSLanguageServer<TState = Self>>>,
}

impl<TPerfLogger: PerfLogger + 'static, TSchemaDocumentation: SchemaDocumentation>
    LSPState<TPerfLogger, TSchemaDocumentation>
{
    /// Private constructor
    pub fn new(
        config: Arc<Config>,
        sender: Sender<Message>,
        task_scheduler: Arc<TaskScheduler<super::Task>>,
        perf_logger: Arc<TPerfLogger>,
        extra_data_provider: Box<dyn LSPExtraDataProvider>,
        schema_documentation_loader: Option<
            Box<dyn SchemaDocumentationLoader<TSchemaDocumentation>>,
        >,
        js_resource: Option<Box<dyn JSLanguageServer<TState = Self>>>,
    ) -> Self {
        debug!("Creating lsp_state...");
        let file_categorizer = FileCategorizer::from_config(&config);
        let root_dir = &config.root_dir.clone();
        let diagnostic_reporter = Arc::new(DiagnosticReporter::new(
            config.root_dir.clone(),
            sender.clone(),
        ));

        let lsp_state = Self {
            config,
            sender,
            task_scheduler,
            diagnostic_reporter,
            extra_data_provider,
            file_categorizer,
            notify_lsp_state_resources: Arc::new(Notify::new()),
            perf_logger,
            project_status: Arc::new(DashMap::with_hasher(FnvBuildHasher::default())),
            root_dir: root_dir.clone(),
            schemas: Arc::new(DashMap::with_hasher(FnvBuildHasher::default())),
            schema_documentation_loader,
            source_programs: Arc::new(DashMap::with_hasher(FnvBuildHasher::default())),
            synced_javascript_features: Default::default(),
            js_resource,
        };

        // Preload schema documentation - this will warm-up schema documentation cache in the LSP Extra Data providers
        // TODO: Find a better place for this
        lsp_state.preload_documentation();

        debug!("Creating lsp_state created!");
        lsp_state
    }

    fn insert_synced_sources(&self, url: &Url, sources: Vec<JavaScriptSourceFeature>) {
        self.synced_javascript_features.insert(url.clone(), sources);
    }

    fn validate_synced_sources(&self, url: &Url) -> LSPRuntimeResult<()> {
        let mut diagnostics = vec![];
        let javascript_features = self.synced_javascript_features.get(url).ok_or_else(|| {
            LSPRuntimeError::UnexpectedError(format!("Expected GraphQL sources for URL {}", url))
        })?;
        let project_name = self.extract_project_name_from_url(url)?;
        let schema = self
            .schemas
            .get(&project_name)
            .ok_or(LSPRuntimeError::ExpectedError)?;

        let mut executable_definitions = vec![];
        let mut docblock_sources = vec![];

        for (index, feature) in javascript_features.iter().enumerate() {
            let source_location_key = SourceLocationKey::embedded(&url.to_string(), index);

            match feature {
                JavaScriptSourceFeature::GraphQL(graphql_source) => {
                    let result = parse_executable_with_error_recovery(
                        &graphql_source.text_source().text,
                        source_location_key,
                    );
                    diagnostics.extend(result.diagnostics.iter().map(|diagnostic| {
                        self.diagnostic_reporter
                            .convert_diagnostic(graphql_source.text_source(), diagnostic)
                    }));

                    let compiler_diagnostics = match build_ir_with_extra_features(
                        &schema,
                        &result.item.definitions,
                        &BuilderOptions {
                            allow_undefined_fragment_spreads: true,
                            fragment_variables_semantic: FragmentVariablesSemantic::PassedValue,
                            relay_mode: Some(RelayMode),
                            default_anonymous_operation_name: None,
                        },
                    )
                    .and_then(|documents| {
                        let mut warnings = vec![];
                        for document in documents {
                            // Today the only warning we check for is deprecated
                            // fields, but in the future we could check for more
                            // things here by making this more generic.
                            warnings.extend(deprecated_fields_for_executable_definition(
                                &schema, &document,
                            )?)
                        }
                        Ok(warnings)
                    }) {
                        Ok(warnings) => warnings,
                        Err(errors) => errors,
                    };

                    diagnostics.extend(compiler_diagnostics.iter().map(|diagnostic| {
                        self.diagnostic_reporter
                            .convert_diagnostic(graphql_source.text_source(), diagnostic)
                    }));

                    executable_definitions.extend(result.item.definitions);
                }
                JavaScriptSourceFeature::Docblock(docblock_source) => {
                    docblock_sources.push(docblock_source);
                }
            }
        }

        let project_config = self.config.projects.get(&project_name).unwrap();
        for (index, docblock_source) in docblock_sources.iter().enumerate() {
            let source_location_key = SourceLocationKey::embedded(url.as_ref(), index);
            let text_source = docblock_source.text_source();
            let text = &text_source.text;
            let result = parse_docblock(text, source_location_key).and_then(|ast| {
                parse_docblock_ast(
                    &ast,
                    Some(&executable_definitions),
                    ParseOptions {
                        enable_output_type: &project_config
                            .feature_flags
                            .relay_resolver_enable_output_type,
                    },
                )
            });

            if let Err(errors) = result {
                diagnostics.extend(errors.iter().map(|diagnostic| {
                    self.diagnostic_reporter
                        .convert_diagnostic(text_source, diagnostic)
                }));
            }
        }
        self.diagnostic_reporter
            .update_quick_diagnostics_for_url(url, diagnostics);

        Ok(())
    }

    fn preload_documentation(&self) {
        for project_config in self.config.enabled_projects() {
            self.get_schema_documentation(&project_config.name.to_string());
        }
    }

    pub fn send_message(&self, message: Message) -> Result<(), SendError<Message>> {
        self.sender.send(message)
    }

    pub fn schedule_task(&self, task: Task) {
        self.task_scheduler.schedule(super::Task::LSPState(task));
    }

    fn process_synced_sources(
        &self,
        uri: &Url,
        sources: Vec<JavaScriptSourceFeature>,
    ) -> LSPRuntimeResult<()> {
        let project_name = self.extract_project_name_from_url(uri)?;

        if let Entry::Vacant(e) = self.project_status.entry(project_name) {
            e.insert(ProjectStatus::Activated);
            self.notify_lsp_state_resources.notify_one();
        }

        self.insert_synced_sources(uri, sources);
        self.schedule_task(Task::ValidateSyncedSource(uri.clone()));

        Ok(())
    }

    fn remove_synced_sources(&self, url: &Url) {
        self.synced_javascript_features.remove(url);
        self.diagnostic_reporter
            .clear_quick_diagnostics_for_url(url);
    }
}

impl<TPerfLogger: PerfLogger + 'static, TSchemaDocumentation: SchemaDocumentation + 'static>
    GlobalState for LSPState<TPerfLogger, TSchemaDocumentation>
{
    type TSchemaDocumentation =
        CombinedSchemaDocumentation<Option<Arc<SDLSchema>>, Option<Arc<TSchemaDocumentation>>>;

    fn root_dir(&self) -> PathBuf {
        self.root_dir.clone()
    }

    fn get_schema(&self, project_name: &StringKey) -> LSPRuntimeResult<Arc<SDLSchema>> {
        self.schemas
            .get(project_name)
            .as_deref()
            .cloned()
            .ok_or_else(|| {
                LSPRuntimeError::UnexpectedError(format!(
                    "get_schema: schema is missing (or not ready, yet) for the `{}` project.",
                    project_name
                ))
            })
    }

    fn get_program(&self, project_name: &StringKey) -> LSPRuntimeResult<Program> {
        self.source_programs
            .get(project_name)
            .map(|p| p.value().clone())
            .ok_or_else(|| {
                LSPRuntimeError::UnexpectedError(format!(
                    "get_program: program is missing (or not ready, yet) for the `{}` project.",
                    project_name
                ))
            })
    }

    fn resolve_node(
        &self,
        text_document_position: &TextDocumentPositionParams,
    ) -> LSPRuntimeResult<FeatureResolutionInfo> {
        let (feature, position_span) = self.extract_feature_from_text(
            text_document_position,
            // For hovering, offset the index by 1
            // ```
            //  field
            //  ^ hover on f
            // ^ position returned by the client
            // ```
            // so the returned cursor is on the char `f`
            1,
        )?;

        let info = match feature {
            Feature::GraphQLDocument(executable_document) => FeatureResolutionInfo::GraphqlNode(
                create_node_resolution_info(executable_document, position_span)?,
            ),
            Feature::DocblockIr(docblock_ir) => FeatureResolutionInfo::DocblockNode(DocblockNode {
                resolution_info: create_docblock_resolution_info(&docblock_ir, position_span)
                    .ok_or(LSPRuntimeError::ExpectedError)?,
                ir: docblock_ir,
            }),
        };
        Ok(info)
    }

    /// Return a parsed executable document for this LSP request, only if the request occurs
    /// within a GraphQL document.
    fn extract_executable_document_from_text(
        &self,
        position: &TextDocumentPositionParams,
        index_offset: usize,
    ) -> LSPRuntimeResult<(ExecutableDocument, Span)> {
        let (feature, span) = self.extract_feature_from_text(position, index_offset)?;
        match feature {
            Feature::GraphQLDocument(document) => Ok((document, span)),
            Feature::DocblockIr(_) => Err(LSPRuntimeError::ExpectedError),
        }
    }

    /// Return a parsed executable document, or parsed Docblock IR for this LSP
    /// request, only if the request occurs within a GraphQL document or Docblock.
    fn extract_feature_from_text(
        &self,
        position: &TextDocumentPositionParams,
        index_offset: usize,
    ) -> LSPRuntimeResult<(Feature, Span)> {
        let project_name = self.extract_project_name_from_url(&position.text_document.uri)?;
        let project_config = self.config.projects.get(&project_name).unwrap();

        extract_feature_from_text(
            project_config,
            &self.synced_javascript_features,
            position,
            index_offset,
        )
    }

    fn get_schema_documentation(&self, schema_name: &str) -> Self::TSchemaDocumentation {
        let primary = self
            .schemas
            .get(&schema_name.intern())
            .map(|x| Arc::clone(x.value()));
        let secondary = self
            .schema_documentation_loader
            .as_ref()
            .map(|loader| loader.get_schema_documentation(schema_name));

        CombinedSchemaDocumentation::new(primary, secondary)
    }

    fn extract_project_name_from_url(&self, url: &Url) -> LSPRuntimeResult<StringKey> {
        extract_project_name_from_url(&self.file_categorizer, url, &self.root_dir)
    }

    fn get_extra_data_provider(&self) -> &dyn LSPExtraDataProvider {
        self.extra_data_provider.as_ref()
    }

    fn resolve_executable_definitions(
        &self,
        text_document_uri: &Url,
    ) -> LSPRuntimeResult<Vec<ExecutableDefinition>> {
        extract_executable_definitions_from_text_document(
            text_document_uri,
            &self.synced_javascript_features,
        )
    }

    fn get_diagnostic_for_range(&self, url: &Url, range: Range) -> Option<Diagnostic> {
        self.diagnostic_reporter
            .get_diagnostics_for_range(url, range)
    }

    fn get_js_language_sever(&self) -> Option<&dyn JSLanguageServer<TState = Self>> {
        self.js_resource.as_deref()
    }

    fn get_full_query_text(
        &self,
        query_text: String,
        project_name: &StringKey,
    ) -> LSPRuntimeResult<String> {
        get_query_text(self, query_text, project_name)
    }

    fn document_opened(&self, uri: &Url, text: &str) -> LSPRuntimeResult<()> {
        if let Some(js_server) = self.get_js_language_sever() {
            js_server.process_js_source(uri, text);
        }

        // First we check to see if this document has any GraphQL documents.
        let embedded_sources = extract_graphql::extract(text);
        if embedded_sources.is_empty() {
            Ok(())
        } else {
            self.process_synced_sources(uri, embedded_sources)
        }
    }

    fn document_changed(&self, uri: &Url, full_text: &str) -> LSPRuntimeResult<()> {
        if let Some(js_server) = self.get_js_language_sever() {
            js_server.process_js_source(uri, full_text);
        }

        // First we check to see if this document has any GraphQL documents.
        let embedded_sources = extract_graphql::extract(full_text);
        if embedded_sources.is_empty() {
            self.remove_synced_sources(uri);
            Ok(())
        } else {
            self.process_synced_sources(uri, embedded_sources)
        }
    }

    fn document_closed(&self, uri: &Url) -> LSPRuntimeResult<()> {
        if let Some(js_server) = self.get_js_language_sever() {
            js_server.remove_js_source(uri);
        }
        self.remove_synced_sources(uri);
        Ok(())
    }

    fn get_content_consumer_type(&self) -> ContentConsumerType {
        ContentConsumerType::Relay
    }
}

#[derive(Debug)]
pub enum Task {
    ValidateSyncedSource(Url),
    ValidateSyncedSources,
}

pub(crate) fn handle_lsp_state_tasks<
    TPerfLogger: PerfLogger + 'static,
    TSchemaDocumentation: SchemaDocumentation + 'static,
>(
    state: Arc<LSPState<TPerfLogger, TSchemaDocumentation>>,
    task: Task,
) {
    match task {
        Task::ValidateSyncedSource(url) => {
            state.validate_synced_sources(&url).ok();
        }
        Task::ValidateSyncedSources => {
            for item in &state.synced_javascript_features {
                state.schedule_task(Task::ValidateSyncedSource(item.key().clone()));
            }
        }
    }
}


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

use common::PerfLogEvent;
use common::PerfLogger;
use dashmap::mapref::entry::Entry;
use fnv::FnvHashMap;
use graphql_ir::FragmentDefinitionNameSet;
use graphql_watchman::WatchmanFileSourceSubscriptionNextChange;
use intern::string_key::StringKey;
use log::debug;
use rayon::iter::ParallelIterator;
use relay_compiler::build_project::get_project_asts;
use relay_compiler::build_project::ProjectAstData;
use relay_compiler::build_project::ProjectAsts;
use relay_compiler::build_raw_program;
use relay_compiler::build_schema;
use relay_compiler::compiler_state::CompilerState;
use relay_compiler::compiler_state::ProjectName;
use relay_compiler::config::ProjectConfig;
use relay_compiler::errors::BuildProjectError;
use relay_compiler::errors::Error;
use relay_compiler::transform_program;
use relay_compiler::validate_program;
use relay_compiler::BuildProjectFailure;
use relay_compiler::FileSource;
use relay_compiler::FileSourceResult;
use relay_compiler::FileSourceSubscription;
use relay_compiler::FileSourceSubscriptionNextChange;
use relay_compiler::GraphQLAsts;
use relay_compiler::SourceControlUpdateStatus;
use schema::SDLSchema;
use schema_documentation::SchemaDocumentation;
use tokio::task;
use tokio::task::JoinHandle;

use super::lsp_state::ProjectStatus;
use super::lsp_state::Task;
use crate::status_updater::set_ready_status;
use crate::status_updater::update_in_progress_status;
use crate::LSPState;

/// This structure is responsible for keeping schemas/programs in sync with the current state of the world
pub(crate) struct LSPStateResources<
    TPerfLogger: PerfLogger + 'static,
    TSchemaDocumentation: SchemaDocumentation + 'static,
> {
    lsp_state: Arc<LSPState<TPerfLogger, TSchemaDocumentation>>,
}

const MAX_ERROR_COUNT: usize = 3;

impl<TPerfLogger: PerfLogger + 'static, TSchemaDocumentation: SchemaDocumentation + 'static>
    LSPStateResources<TPerfLogger, TSchemaDocumentation>
{
    pub(crate) fn new(lsp_state: Arc<LSPState<TPerfLogger, TSchemaDocumentation>>) -> Self {
        Self { lsp_state }
    }

    pub(crate) fn watch(self) {
        tokio::spawn(async move {
            // Wait for a notify from VSCode document events. This is for preventing
            // starting the loop when the current workspace doesn't have any Relay file.
            self.lsp_state.notify_lsp_state_resources.notified().await;
            self.internal_watch().await;
        });
    }

    /// Create an end-less loop of keeping the resources up-to-date with the source control changes
    async fn internal_watch(&self) {
        // avoid dead loop when watchman has an error
        let mut error_count = 0;
        'outer: loop {
            debug!("Initializing resources for LSP server");

            update_in_progress_status(
                "Relay: watchman...",
                Some("Sending watchman query to get source files and possible saved state"),
                &self.lsp_state.sender,
            );
            let setup_event = self
                .lsp_state
                .perf_logger
                .create_event("lsp_state_initialize_resources");
            let timer = setup_event.start("lsp_state_initialize_resources_time");

            let file_source = match FileSource::connect(&self.lsp_state.config, &setup_event).await
            {
                Ok(f) => f,
                Err(error) => {
                    self.log_errors("watch_build_error", &error);
                    error_count += 1;
                    if error_count == MAX_ERROR_COUNT {
                        panic!("{}", error);
                    }
                    continue;
                }
            };
            let (mut compiler_state, file_source_subscription) = match file_source
                .subscribe(&setup_event, self.lsp_state.perf_logger.as_ref())
                .await
            {
                Ok(f) => f,
                Err(error) => {
                    self.log_errors("watch_build_error", &error);
                    error_count += 1;
                    if error_count == MAX_ERROR_COUNT {
                        panic!("{}", error);
                    }
                    continue;
                }
            };

            let pending_file_source_changes =
                Arc::clone(&compiler_state.pending_file_source_changes);

            // Start a separate task to receive watchman subscription updates
            // this process will notify, when the change is detected.
            // If watchman detects the source control update, it will update the state
            // of the source_control_update_status, so we can cancel building schemas/
            // and programs earlier
            let subscription_handle = self.watchman_subscription_handler(
                file_source_subscription,
                pending_file_source_changes,
                Arc::clone(&compiler_state.source_control_update_status),
            );
            update_in_progress_status(
                "Relay: creating state...",
                Some("Building schemas and source programs for LSP"),
                &self.lsp_state.sender,
            );

            self.lsp_state
                .diagnostic_reporter
                .clear_regular_diagnostics();

            // Run initial build, before entering the watch changes loop
            if let Err(error) = self.build_projects(&mut compiler_state, &setup_event) {
                self.publish_errors(&error, "lsp_state_error");
            }
            set_ready_status(&self.lsp_state.sender);

            setup_event.stop(timer);
            setup_event.complete();

            debug!("LSP server initialization completed!");
            error_count = 0;

            // Here we will wait for changes from watchman
            'inner: loop {
                // Wait for a notify from watchman updates, or when a Relay file
                // from an unactivated project is opened in VSCode
                self.lsp_state.notify_lsp_state_resources.notified().await;

                // Source control update started, we can ignore all pending changes, and wait for it to complete,
                // we may change the status bar to `Source Control Update...`
                if compiler_state.source_control_update_status.is_started() {
                    update_in_progress_status(
                        "Relay: hg update...",
                        Some("Waiting for source control update"),
                        &self.lsp_state.sender,
                    );
                    continue 'inner;
                }

                // SC Update completed, we need to abort current subscription, and re-initialize resource for LSP
                if compiler_state.source_control_update_status.is_completed() {
                    debug!("Watchman indicated the the source control update has completed!");
                    subscription_handle.abort();
                    continue 'outer;
                }

                let log_event = self
                    .lsp_state
                    .perf_logger
                    .create_event("lsp_state_watchman_event");
                let log_time = log_event.start("lsp_state_watchman_event_time");

                if let Err(error) = self.incremental_build(&mut compiler_state, &log_event) {
                    self.publish_errors(&error, "lsp_state_user_error");
                }
                set_ready_status(&self.lsp_state.sender);

                log_event.stop(log_time);
                log_event.complete();
            }
        }
    }

    fn incremental_build(
        &self,
        compiler_state: &mut CompilerState,
        log_event: &impl PerfLogEvent,
    ) -> Result<(), Error> {
        let has_new_changes = compiler_state.merge_file_source_changes(
            &self.lsp_state.config,
            self.lsp_state.perf_logger.as_ref(),
            false,
        )?;

        // Rebuild if there are pending files or if a new project is activated
        if has_new_changes
            || self
                .lsp_state
                .project_status
                .iter()
                .any(|r| r.value() == &ProjectStatus::Activated)
        {
            debug!("LSP server detected changes...");

            self.lsp_state
                .diagnostic_reporter
                .clear_regular_diagnostics();

            update_in_progress_status(
                "Relay: checking...",
                Some("Validating changes, and updating source programs with the latest changes."),
                &self.lsp_state.sender,
            );

            self.build_projects(compiler_state, log_event)?;
        }

        Ok(())
    }

    fn build_projects(
        &self,
        compiler_state: &mut CompilerState,
        log_event: &impl PerfLogEvent,
    ) -> Result<(), Error> {
        let graphql_asts = log_event.time("parse_sources_time", || {
            GraphQLAsts::from_graphql_sources_map(
                &compiler_state.graphql_sources,
                &compiler_state.get_dirty_definitions(&self.lsp_state.config),
            )
        })?;

        if compiler_state.should_cancel_current_build() {
            debug!("Build is cancelled: new file changes are pending.");
            return Err(Error::Cancelled);
        }

        let timer = log_event.start("build_lsp_projects");
        let build_results: Vec<_> = self
            .lsp_state
            .config
            .par_enabled_projects()
            .filter(|project_config| {
                // Filter inactive projects
                if !self
                    .lsp_state
                    .project_status
                    .contains_key(&project_config.name)
                {
                    return false;
                }
                // When the source programs is empty, we need to compile all source programs once
                if !self
                    .lsp_state
                    .source_programs
                    .contains_key(&project_config.name)
                {
                    return true;
                }
                if let Some(base) = project_config.base {
                    if compiler_state.project_has_pending_changes(base) {
                        return true;
                    }
                }
                compiler_state.project_has_pending_changes(project_config.name)
            })
            .map(|project_config| self.build_project(project_config, compiler_state, &graphql_asts))
            .collect();
        log_event.stop(timer);

        let mut errors = vec![];
        for build_result in build_results {
            match build_result {
                Err(BuildProjectFailure::Error(err)) => {
                    errors.push(err);
                }
                _ => {}
            }
        }
        if errors.is_empty() {
            compiler_state.complete_compilation();
            Ok(())
        } else {
            Err(Error::BuildProjectsErrors { errors })
        }
    }

    fn build_project(
        &self,
        project_config: &ProjectConfig,
        compiler_state: &CompilerState,
        graphql_asts_map: &FnvHashMap<ProjectName, GraphQLAsts>,
    ) -> Result<StringKey, BuildProjectFailure> {
        self.lsp_state
            .project_status
            .insert(project_config.name, ProjectStatus::Completed);
        let log_event = self.lsp_state.perf_logger.create_event("build_lsp_project");
        let project_name = project_config.name;
        let build_time = log_event.start("build_lsp_project_time");
        log_event.string("project", project_name.to_string());

        let schema = log_event.time("build_schema_time", || {
            self.build_schema(compiler_state, project_config, graphql_asts_map)
        })?;

        let ProjectAstData {
            project_asts,
            base_fragment_names,
        } = get_project_asts(&schema, graphql_asts_map, project_config)?;

        // This will kick-off the validation for all synced sources
        self.lsp_state.schedule_task(Task::ValidateSyncedSources);

        self.build_programs(
            project_config,
            project_asts,
            base_fragment_names,
            compiler_state,
            graphql_asts_map,
            schema,
            &log_event,
        )?;

        log_event.stop(build_time);
        Ok(project_name)
    }

    fn build_schema(
        &self,
        compiler_state: &CompilerState,
        project_config: &ProjectConfig,
        graphql_asts_map: &FnvHashMap<ProjectName, GraphQLAsts>,
    ) -> Result<Arc<SDLSchema>, BuildProjectFailure> {
        match self.lsp_state.schemas.entry(project_config.name) {
            Entry::Vacant(e) => {
                let schema = build_schema(compiler_state, project_config, graphql_asts_map)
                    .map_err(|errors| {
                        BuildProjectFailure::Error(BuildProjectError::ValidationErrors {
                            errors,
                            project_name: project_config.name,
                        })
                    })?;
                e.insert(Arc::clone(&schema));
                Ok(schema)
            }
            Entry::Occupied(mut e) => {
                if !compiler_state.project_has_pending_schema_changes(project_config.name) {
                    Ok(Arc::clone(e.get()))
                } else {
                    let schema = build_schema(compiler_state, project_config, graphql_asts_map)
                        .map_err(|errors| {
                            debug!("build error");
                            BuildProjectFailure::Error(BuildProjectError::ValidationErrors {
                                errors,
                                project_name: project_config.name,
                            })
                        })?;
                    e.insert(Arc::clone(&schema));
                    Ok(schema)
                }
            }
        }
    }

    fn build_programs(
        &self,
        project_config: &ProjectConfig,
        project_asts: ProjectAsts,
        base_fragment_names: FragmentDefinitionNameSet,
        compiler_state: &CompilerState,
        graphql_asts: &FnvHashMap<ProjectName, GraphQLAsts>,
        schema: Arc<SDLSchema>,
        log_event: &impl PerfLogEvent,
    ) -> Result<(), BuildProjectFailure> {
        let is_incremental_build = self
            .lsp_state
            .source_programs
            .contains_key(&project_config.name)
            && compiler_state.has_processed_changes()
            && !compiler_state
                .has_breaking_schema_change(project_config.name, &project_config.schema_config)
            && if let Some(base) = project_config.base {
                !compiler_state.has_breaking_schema_change(base, &project_config.schema_config)
            } else {
                true
            };

        let (base_program, _) = build_raw_program(
            project_config,
            project_asts,
            schema,
            log_event,
            is_incremental_build,
        )?;

        if compiler_state.should_cancel_current_build() {
            debug!("Build is cancelled: updates in source code/or new file changes are pending.");
            return Err(BuildProjectFailure::Cancelled);
        }

        match self.lsp_state.source_programs.entry(project_config.name) {
            Entry::Vacant(e) => {
                e.insert(base_program.clone());
            }
            Entry::Occupied(mut e) => {
                let program = e.get_mut();
                let removed_definition_names = graphql_asts
                    .get(&project_config.name)
                    .map(|ast| ast.removed_definition_names.as_ref());
                program.merge_program(&base_program, removed_definition_names);
            }
        }

        // Call validation rules that go beyond type checking.
        validate_program(
            &self.lsp_state.config,
            project_config,
            &base_program,
            log_event,
        )?;

        transform_program(
            project_config,
            Arc::new(base_program),
            Arc::new(base_fragment_names),
            Arc::clone(&self.lsp_state.perf_logger),
            log_event,
            self.lsp_state.config.custom_transforms.as_ref(),
        )?;
        Ok(())
    }

    fn watchman_subscription_handler(
        &self,
        mut file_source_subscription: FileSourceSubscription,
        pending_file_source_changes: Arc<RwLock<Vec<FileSourceResult>>>,
        source_code_update_status: Arc<SourceControlUpdateStatus>,
    ) -> JoinHandle<()> {
        let notify_sender = self.lsp_state.notify_lsp_state_resources.clone();
        task::spawn(async move {
            loop {
                match file_source_subscription.next_change().await {
                    Err(_) => {
                        // do nothing? compiler should panic, and restart the lsp
                    }
                    Ok(FileSourceSubscriptionNextChange::Watchman(watchman_next_change)) => {
                        match watchman_next_change {
                            WatchmanFileSourceSubscriptionNextChange::None => {}
                            WatchmanFileSourceSubscriptionNextChange::SourceControlUpdateEnter => {
                                source_code_update_status.mark_as_started();
                            }
                            WatchmanFileSourceSubscriptionNextChange::SourceControlUpdateLeave => {
                                source_code_update_status.set_to_default();
                            }
                            WatchmanFileSourceSubscriptionNextChange::SourceControlUpdate => {
                                source_code_update_status.mark_as_completed();
                                notify_sender.notify_one();
                                break;
                            }
                            WatchmanFileSourceSubscriptionNextChange::Result(
                                file_source_changes,
                            ) => {
                                pending_file_source_changes
                                .write()
                                .expect("LSPState::watch_and_update_schemas: expect to acquire write lock on pending_file_source_changes")
                                .push(FileSourceResult::Watchman(file_source_changes));

                                notify_sender.notify_one();
                            }
                        }
                    }
                }
            }
        })
    }

    fn log_errors(&self, log_event_name: &'static str, error: &Error) {
        let error_event = self.lsp_state.perf_logger.create_event(log_event_name);
        error_event.string("error", error.to_string());
        error_event.complete();
    }

    /// Log errors and report the diagnostics to IDE
    fn publish_errors(&self, error: &Error, log_event_name: &'static str) {
        self.lsp_state.diagnostic_reporter.report_error(error);
        self.lsp_state.diagnostic_reporter.commit_diagnostics();
        self.log_errors(log_event_name, error)
    }
}


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

mod heartbeat;
mod lsp_notification_dispatch;
mod lsp_request_dispatch;
mod lsp_state;
mod lsp_state_resources;
mod task_queue;

use std::ops::ControlFlow;
use std::sync::Arc;

use common::PerfLogEvent;
use common::PerfLogger;
use crossbeam::channel::Receiver;
use crossbeam::select;
use heartbeat::on_heartbeat;
use heartbeat::HeartbeatRequest;
use log::debug;
pub use lsp_notification_dispatch::LSPNotificationDispatch;
pub use lsp_request_dispatch::LSPRequestDispatch;
use lsp_server::Connection;
use lsp_server::ErrorCode;
use lsp_server::Message;
use lsp_server::Notification;
use lsp_server::Response as ServerResponse;
use lsp_server::ResponseError;
pub use lsp_state::GlobalState;
pub use lsp_state::LSPState;
pub use lsp_state::Schemas;
pub use lsp_state::SourcePrograms;
use lsp_types::notification::Cancel;
use lsp_types::notification::DidChangeTextDocument;
use lsp_types::notification::DidCloseTextDocument;
use lsp_types::notification::DidOpenTextDocument;
use lsp_types::notification::DidSaveTextDocument;
use lsp_types::notification::Exit;
use lsp_types::request::CodeActionRequest;
use lsp_types::request::Completion;
use lsp_types::request::GotoDefinition;
use lsp_types::request::HoverRequest;
use lsp_types::request::References;
use lsp_types::request::ResolveCompletionItem;
use lsp_types::request::Shutdown;
use lsp_types::CodeActionProviderCapability;
use lsp_types::CompletionOptions;
use lsp_types::InitializeParams;
use lsp_types::ServerCapabilities;
use lsp_types::TextDocumentSyncCapability;
use lsp_types::TextDocumentSyncKind;
use lsp_types::WorkDoneProgressOptions;
use relay_compiler::config::Config;
use relay_compiler::NoopArtifactWriter;
use schema_documentation::SchemaDocumentation;
use schema_documentation::SchemaDocumentationLoader;

use self::task_queue::TaskProcessor;
use crate::code_action::on_code_action;
use crate::completion::on_completion;
use crate::completion::on_resolve_completion_item;
use crate::explore_schema_for_type::on_explore_schema_for_type;
use crate::explore_schema_for_type::ExploreSchemaForType;
use crate::find_field_usages::on_find_field_usages;
use crate::find_field_usages::FindFieldUsages;
use crate::goto_definition::on_get_source_location_of_type_definition;
use crate::goto_definition::on_goto_definition;
use crate::goto_definition::GetSourceLocationOfTypeDefinition;
use crate::graphql_tools::on_graphql_execute_query;
use crate::graphql_tools::GraphQLExecuteQuery;
use crate::hover::on_hover;
use crate::js_language_server::JSLanguageServer;
use crate::lsp_process_error::LSPProcessResult;
use crate::lsp_runtime_error::LSPRuntimeError;
use crate::references::on_references;
use crate::resolved_types_at_location::on_get_resolved_types_at_location;
use crate::resolved_types_at_location::ResolvedTypesAtLocation;
use crate::search_schema_items::on_search_schema_items;
use crate::search_schema_items::SearchSchemaItems;
use crate::server::lsp_state::handle_lsp_state_tasks;
use crate::server::lsp_state_resources::LSPStateResources;
use crate::server::task_queue::TaskQueue;
use crate::shutdown::on_exit;
use crate::shutdown::on_shutdown;
use crate::status_reporter::LSPStatusReporter;
use crate::text_documents::on_cancel;
use crate::text_documents::on_did_change_text_document;
use crate::text_documents::on_did_close_text_document;
use crate::text_documents::on_did_open_text_document;
use crate::text_documents::on_did_save_text_document;
pub use crate::LSPExtraDataProvider;

/// Initializes an LSP connection, handling the `initialize` message and `initialized` notification
/// handshake.
pub fn initialize(connection: &Connection) -> LSPProcessResult<InitializeParams> {
    let server_capabilities = ServerCapabilities {
        // Enable text document syncing so we can know when files are opened/changed/saved/closed
        text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),

        completion_provider: Some(CompletionOptions {
            resolve_provider: Some(true),
            trigger_characters: Some(vec!["(".into(), "\n".into(), ",".into(), "@".into()]),
            work_done_progress_options: WorkDoneProgressOptions {
                work_done_progress: None,
            },
            ..Default::default()
        }),

        hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
        definition_provider: Some(lsp_types::OneOf::Left(true)),
        references_provider: Some(lsp_types::OneOf::Left(true)),
        code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
        ..Default::default()
    };

    let server_capabilities = serde_json::to_value(&server_capabilities)?;
    let params = connection.initialize(server_capabilities)?;
    let params: InitializeParams = serde_json::from_value(params)?;
    Ok(params)
}

#[derive(Debug)]
pub enum Task {
    InboundMessage(lsp_server::Message),
    LSPState(lsp_state::Task),
}

/// Run the main server loop
pub async fn run<
    TPerfLogger: PerfLogger + 'static,
    TSchemaDocumentation: SchemaDocumentation + 'static,
>(
    connection: Connection,
    mut config: Config,
    _params: InitializeParams,
    perf_logger: Arc<TPerfLogger>,
    extra_data_provider: Box<dyn LSPExtraDataProvider + Send + Sync>,
    schema_documentation_loader: Option<Box<dyn SchemaDocumentationLoader<TSchemaDocumentation>>>,
    js_resource: Option<
        Box<dyn JSLanguageServer<TState = LSPState<TPerfLogger, TSchemaDocumentation>>>,
    >,
) -> LSPProcessResult<()>
where
    TPerfLogger: PerfLogger + 'static,
{
    debug!(
        "Running language server with config root {:?}",
        config.root_dir
    );

    let task_processor = LSPTaskProcessor;
    let task_queue = TaskQueue::new(Arc::new(task_processor));
    let task_scheduler = task_queue.get_scheduler();

    config.artifact_writer = Box::new(NoopArtifactWriter);
    config.status_reporter = Box::new(LSPStatusReporter::new(
        config.root_dir.clone(),
        connection.sender.clone(),
    ));

    let lsp_state = Arc::new(LSPState::new(
        Arc::new(config),
        connection.sender.clone(),
        Arc::clone(&task_scheduler),
        Arc::clone(&perf_logger),
        extra_data_provider,
        schema_documentation_loader,
        js_resource,
    ));

    LSPStateResources::new(Arc::clone(&lsp_state)).watch();

    while let Some(task) = next_task(&connection.receiver, &task_queue.receiver) {
        task_queue.process(Arc::clone(&lsp_state), task);
    }

    panic!("Client exited without proper shutdown sequence.")
}

fn next_task(
    lsp_receiver: &Receiver<Message>,
    task_queue_receiver: &Receiver<Task>,
) -> Option<Task> {
    select! {
        recv(lsp_receiver) -> message => message.ok().map(Task::InboundMessage),
        recv(task_queue_receiver) -> task => task.ok()
    }
}

struct LSPTaskProcessor;

impl<TPerfLogger: PerfLogger + 'static, TSchemaDocumentation: SchemaDocumentation + 'static>
    TaskProcessor<LSPState<TPerfLogger, TSchemaDocumentation>, Task> for LSPTaskProcessor
{
    fn process(&self, state: Arc<LSPState<TPerfLogger, TSchemaDocumentation>>, task: Task) {
        match task {
            Task::InboundMessage(Message::Request(request)) => handle_request(state, request),
            Task::InboundMessage(Message::Notification(notification)) => {
                handle_notification(state, notification);
            }
            Task::LSPState(lsp_task) => {
                handle_lsp_state_tasks(state, lsp_task);
            }
            Task::InboundMessage(Message::Response(_)) => {
                // TODO: handle response from the client -> cancel message, etc
            }
        }
    }
}

fn handle_request<TPerfLogger: PerfLogger + 'static, TSchemaDocumentation: SchemaDocumentation>(
    lsp_state: Arc<LSPState<TPerfLogger, TSchemaDocumentation>>,
    request: lsp_server::Request,
) {
    debug!("request received {:?}", request);
    let get_server_response_bound = |req| dispatch_request(req, lsp_state.as_ref());
    let get_response = with_request_logging(&lsp_state.perf_logger, get_server_response_bound);

    lsp_state
        .send_message(Message::Response(get_response(request)))
        .expect("Unable to send message to a client.");
}

fn dispatch_request(request: lsp_server::Request, lsp_state: &impl GlobalState) -> ServerResponse {
    // Returns ControlFlow::Break(ServerResponse) if the request
    // was handled, ControlFlow::Continue(Request) otherwise.
    let get_response = || {
        let request = LSPRequestDispatch::new(request, lsp_state)
            .on_request_sync::<ResolvedTypesAtLocation>(on_get_resolved_types_at_location)?
            .on_request_sync::<SearchSchemaItems>(on_search_schema_items)?
            .on_request_sync::<ExploreSchemaForType>(on_explore_schema_for_type)?
            .on_request_sync::<GetSourceLocationOfTypeDefinition>(
                on_get_source_location_of_type_definition,
            )?
            .on_request_sync::<HoverRequest>(on_hover)?
            .on_request_sync::<GotoDefinition>(on_goto_definition)?
            .on_request_sync::<References>(on_references)?
            .on_request_sync::<Completion>(on_completion)?
            .on_request_sync::<ResolveCompletionItem>(on_resolve_completion_item)?
            .on_request_sync::<CodeActionRequest>(on_code_action)?
            .on_request_sync::<Shutdown>(on_shutdown)?
            .on_request_sync::<GraphQLExecuteQuery>(on_graphql_execute_query)?
            .on_request_sync::<HeartbeatRequest>(on_heartbeat)?
            .on_request_sync::<FindFieldUsages>(on_find_field_usages)?
            .request();

        // If we have gotten here, we have not handled the request
        ControlFlow::Continue(request)
    };

    match get_response() {
        ControlFlow::Break(response) => response,
        ControlFlow::Continue(request) => ServerResponse {
            id: request.id,
            result: None,
            error: Some(ResponseError {
                code: ErrorCode::MethodNotFound as i32,
                data: None,
                message: format!("No handler registered for method '{}'", request.method),
            }),
        },
    }
}

fn with_request_logging<'a, TPerfLogger: PerfLogger + 'static>(
    perf_logger: &'a Arc<TPerfLogger>,
    get_response: impl FnOnce(lsp_server::Request) -> ServerResponse + 'a,
) -> impl FnOnce(lsp_server::Request) -> ServerResponse + 'a {
    move |request| {
        let lsp_request_event = perf_logger.create_event("lsp_message");
        lsp_request_event.string("lsp_method", request.method.clone());
        lsp_request_event.string("lsp_type", "request".to_string());
        let lsp_request_processing_time = lsp_request_event.start("lsp_message_processing_time");

        let response = get_response(request);

        if response.result.is_some() {
            lsp_request_event.string("lsp_outcome", "success".to_string());
        } else if let Some(error) = &response.error {
            if error.code == ErrorCode::RequestCanceled as i32 {
                lsp_request_event.string("lsp_outcome", "canceled".to_string());
            } else {
                lsp_request_event.string("lsp_outcome", "error".to_string());
                lsp_request_event.string("lsp_error_message", error.message.to_string());
                if let Some(data) = &error.data {
                    lsp_request_event.string("lsp_error_data", data.to_string());
                }
            }
        }
        // N.B. we don't handle the case where the ServerResponse has neither a result nor
        // an error, which is an invalid state.

        lsp_request_event.stop(lsp_request_processing_time);
        lsp_request_event.complete();

        response
    }
}

fn handle_notification<
    TPerfLogger: PerfLogger + 'static,
    TSchemaDocumentation: SchemaDocumentation,
>(
    lsp_state: Arc<LSPState<TPerfLogger, TSchemaDocumentation>>,
    notification: Notification,
) {
    debug!("notification received {:?}", notification);
    let lsp_notification_event = lsp_state.perf_logger.create_event("lsp_message");
    lsp_notification_event.string("lsp_method", notification.method.clone());
    lsp_notification_event.string("lsp_type", "notification".to_string());
    let lsp_notification_processing_time =
        lsp_notification_event.start("lsp_message_processing_time");

    let notification_result = dispatch_notification(notification, lsp_state.as_ref());

    match notification_result {
        ControlFlow::Continue(()) => {
            // The notification is not handled
            lsp_notification_event.string("lsp_outcome", "error".to_string());
        }
        ControlFlow::Break(err) => {
            if let Some(err) = err {
                lsp_notification_event.string("lsp_outcome", "error".to_string());
                if let LSPRuntimeError::UnexpectedError(message) = err {
                    lsp_notification_event.string("lsp_error_message", message);
                }
            } else {
                lsp_notification_event.string("lsp_outcome", "success".to_string());
            }
        }
    }

    lsp_notification_event.stop(lsp_notification_processing_time);
    lsp_notification_event.complete();
}

fn dispatch_notification(
    notification: lsp_server::Notification,
    lsp_state: &impl GlobalState,
) -> ControlFlow<Option<LSPRuntimeError>, ()> {
    // Returns ControlFlow::Break(Option<LSPRuntimeError>) if the notification
    // was handled, ControlFlow::Continue(()) otherwise.
    let notification = LSPNotificationDispatch::new(notification, lsp_state)
        .on_notification_sync::<DidOpenTextDocument>(on_did_open_text_document)?
        .on_notification_sync::<DidCloseTextDocument>(on_did_close_text_document)?
        .on_notification_sync::<DidChangeTextDocument>(on_did_change_text_document)?
        .on_notification_sync::<DidSaveTextDocument>(on_did_save_text_document)?
        .on_notification_sync::<Cancel>(on_cancel)?
        .on_notification_sync::<Exit>(on_exit)?
        .notification();

    // If we have gotten here, we have not handled the notification
    debug!(
        "Error: no handler registered for notification '{}'",
        notification.method
    );
    ControlFlow::Continue(())
}


/*
 * 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 std::thread;
use std::time::Instant;

use crossbeam::channel::unbounded;
use crossbeam::channel::Receiver;
use crossbeam::channel::Sender;
use log::debug;

pub struct TaskQueue<S, T> {
    processor: Arc<dyn TaskProcessor<S, T>>,
    pub receiver: Receiver<T>,
    scheduler: Arc<TaskScheduler<T>>,
}

pub trait TaskProcessor<S, T>: Send + Sync + 'static {
    fn process(&self, state: Arc<S>, task: T);
}

pub struct TaskScheduler<T> {
    sender: Sender<T>,
}

impl<T> TaskScheduler<T> {
    pub fn schedule(&self, task: T) {
        self.sender
            .send(task)
            .expect("Unable to send task to a channel.")
    }
}

impl<S, T> TaskQueue<S, T>
where
    S: Send + Sync + 'static,
    T: Send + Sync + std::fmt::Debug + 'static,
{
    pub fn new(processor: Arc<dyn TaskProcessor<S, T>>) -> Self {
        let (sender, receiver) = unbounded();

        Self {
            processor,
            receiver,
            scheduler: Arc::new(TaskScheduler { sender }),
        }
    }

    pub fn get_scheduler(&self) -> Arc<TaskScheduler<T>> {
        Arc::clone(&self.scheduler)
    }

    pub fn process(&self, state: Arc<S>, task: T) {
        let task_str = format!("{:?}", &task);
        let now = Instant::now();
        debug!("Processing task {:?}", &task_str);
        let processor = Arc::clone(&self.processor);
        thread::spawn(move || {
            processor.process(state, task);
            debug!(
                "task {} completed in {}ms",
                task_str,
                now.elapsed().as_millis()
            );
        });
    }
}


/*
 * 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.
 *
 * @generated SignedSource<<1ca9d7eb11468f905882356aaeae390b>>
 */

mod find_field_usages;

use find_field_usages::transform_fixture;
use fixture_tests::test_fixture;

#[test]
fn find_subtype() {
    let input = include_str!("find_field_usages/fixtures/find_subtype.graphql");
    let expected = include_str!("find_field_usages/fixtures/find_subtype.expected");
    test_fixture(transform_fixture, "find_subtype.graphql", "find_field_usages/fixtures/find_subtype.expected", input, expected);
}

#[test]
fn find_supertype() {
    let input = include_str!("find_field_usages/fixtures/find_supertype.graphql");
    let expected = include_str!("find_field_usages/fixtures/find_supertype.expected");
    test_fixture(transform_fixture, "find_supertype.graphql", "find_field_usages/fixtures/find_supertype.expected", input, expected);
}

#[test]
fn fragment_field() {
    let input = include_str!("find_field_usages/fixtures/fragment_field.graphql");
    let expected = include_str!("find_field_usages/fixtures/fragment_field.expected");
    test_fixture(transform_fixture, "fragment_field.graphql", "find_field_usages/fixtures/fragment_field.expected", input, expected);
}

#[test]
fn fragment_field_nomatch() {
    let input = include_str!("find_field_usages/fixtures/fragment_field.nomatch.graphql");
    let expected = include_str!("find_field_usages/fixtures/fragment_field.nomatch.expected");
    test_fixture(transform_fixture, "fragment_field.nomatch.graphql", "find_field_usages/fixtures/fragment_field.nomatch.expected", input, expected);
}

#[test]
fn inline_fragment() {
    let input = include_str!("find_field_usages/fixtures/inline_fragment.graphql");
    let expected = include_str!("find_field_usages/fixtures/inline_fragment.expected");
    test_fixture(transform_fixture, "inline_fragment.graphql", "find_field_usages/fixtures/inline_fragment.expected", input, expected);
}

#[test]
fn inline_fragment_also_matches_outer_type() {
    let input = include_str!("find_field_usages/fixtures/inline_fragment_also_matches_outer_type.graphql");
    let expected = include_str!("find_field_usages/fixtures/inline_fragment_also_matches_outer_type.expected");
    test_fixture(transform_fixture, "inline_fragment_also_matches_outer_type.graphql", "find_field_usages/fixtures/inline_fragment_also_matches_outer_type.expected", input, expected);
}

#[test]
fn linked_field() {
    let input = include_str!("find_field_usages/fixtures/linked_field.graphql");
    let expected = include_str!("find_field_usages/fixtures/linked_field.expected");
    test_fixture(transform_fixture, "linked_field.graphql", "find_field_usages/fixtures/linked_field.expected", input, expected);
}

#[test]
fn linked_field_nomatch() {
    let input = include_str!("find_field_usages/fixtures/linked_field.nomatch.graphql");
    let expected = include_str!("find_field_usages/fixtures/linked_field.nomatch.expected");
    test_fixture(transform_fixture, "linked_field.nomatch.graphql", "find_field_usages/fixtures/linked_field.nomatch.expected", input, expected);
}

#[test]
fn multiple_matches() {
    let input = include_str!("find_field_usages/fixtures/multiple_matches.graphql");
    let expected = include_str!("find_field_usages/fixtures/multiple_matches.expected");
    test_fixture(transform_fixture, "multiple_matches.graphql", "find_field_usages/fixtures/multiple_matches.expected", input, expected);
}

#[test]
fn query_field() {
    let input = include_str!("find_field_usages/fixtures/query_field.graphql");
    let expected = include_str!("find_field_usages/fixtures/query_field.expected");
    test_fixture(transform_fixture, "query_field.graphql", "find_field_usages/fixtures/query_field.expected", input, expected);
}


/*
 * 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.
 *
 * @generated SignedSource<<7e6c48eb62c1d815fb6644cb2e6e3bb3>>
 */

mod hover;

use hover::transform_fixture;
use fixture_tests::test_fixture;

#[test]
fn double_underscore_id_field() {
    let input = include_str!("hover/fixtures/double_underscore_id_field.graphql");
    let expected = include_str!("hover/fixtures/double_underscore_id_field.expected");
    test_fixture(transform_fixture, "double_underscore_id_field.graphql", "hover/fixtures/double_underscore_id_field.expected", input, expected);
}

#[test]
fn double_underscore_typename_field() {
    let input = include_str!("hover/fixtures/double_underscore_typename_field.graphql");
    let expected = include_str!("hover/fixtures/double_underscore_typename_field.expected");
    test_fixture(transform_fixture, "double_underscore_typename_field.graphql", "hover/fixtures/double_underscore_typename_field.expected", input, expected);
}

#[test]
fn fragment_definition_name() {
    let input = include_str!("hover/fixtures/fragment_definition_name.graphql");
    let expected = include_str!("hover/fixtures/fragment_definition_name.expected");
    test_fixture(transform_fixture, "fragment_definition_name.graphql", "hover/fixtures/fragment_definition_name.expected", input, expected);
}

#[test]
fn fragment_spread() {
    let input = include_str!("hover/fixtures/fragment_spread.graphql");
    let expected = include_str!("hover/fixtures/fragment_spread.expected");
    test_fixture(transform_fixture, "fragment_spread.graphql", "hover/fixtures/fragment_spread.expected", input, expected);
}

#[test]
fn scalar_field_from_client_schema_extension() {
    let input = include_str!("hover/fixtures/scalar_field_from_client_schema_extension.graphql");
    let expected = include_str!("hover/fixtures/scalar_field_from_client_schema_extension.expected");
    test_fixture(transform_fixture, "scalar_field_from_client_schema_extension.graphql", "hover/fixtures/scalar_field_from_client_schema_extension.expected", input, expected);
}

#[test]
fn scalar_field_from_relay_resolver() {
    let input = include_str!("hover/fixtures/scalar_field_from_relay_resolver.graphql");
    let expected = include_str!("hover/fixtures/scalar_field_from_relay_resolver.expected");
    test_fixture(transform_fixture, "scalar_field_from_relay_resolver.graphql", "hover/fixtures/scalar_field_from_relay_resolver.expected", input, expected);
}

#[test]
fn scalar_field_with_description() {
    let input = include_str!("hover/fixtures/scalar_field_with_description.graphql");
    let expected = include_str!("hover/fixtures/scalar_field_with_description.expected");
    test_fixture(transform_fixture, "scalar_field_with_description.graphql", "hover/fixtures/scalar_field_with_description.expected", input, expected);
}

#[test]
fn whitespace_after_query_selection() {
    let input = include_str!("hover/fixtures/whitespace_after_query_selection.graphql");
    let expected = include_str!("hover/fixtures/whitespace_after_query_selection.expected");
    test_fixture(transform_fixture, "whitespace_after_query_selection.graphql", "hover/fixtures/whitespace_after_query_selection.expected", input, expected);
}

#[test]
fn whitespace_within_linked_field_selection() {
    let input = include_str!("hover/fixtures/whitespace_within_linked_field_selection.graphql");
    let expected = include_str!("hover/fixtures/whitespace_within_linked_field_selection.expected");
    test_fixture(transform_fixture, "whitespace_within_linked_field_selection.graphql", "hover/fixtures/whitespace_within_linked_field_selection.expected", input, expected);
}


/*
 * 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::SourceLocationKey;
use fixture_tests::Fixture;
use graphql_ir::build;
use graphql_ir::reexport::Intern;
use graphql_ir::Program;
use graphql_syntax::parse_executable;
use relay_lsp::find_field_usages;
use relay_test_schema::get_test_schema;

pub fn transform_fixture(fixture: &Fixture<'_>) -> Result<String, String> {
    let parts: Vec<_> = fixture.content.split("%arguments%").collect();
    if let [document, arguments] = parts.as_slice() {
        if let [type_name, field_name] = arguments
            .split_ascii_whitespace()
            .collect::<Vec<&str>>()
            .as_slice()
        {
            let source_location = SourceLocationKey::standalone(fixture.file_name);
            let schema = get_test_schema();
            let ast = parse_executable(document, source_location).unwrap();
            let ir = build(&schema, &ast.definitions).unwrap();
            let program = Program::from_definitions(schema, ir);

            let result = find_field_usages::get_usages(
                &program,
                &get_test_schema(),
                type_name.intern(),
                field_name.intern(),
            )
            .unwrap()
            .into_iter()
            .map(|location| format!("{:?}\n", location))
            .collect::<Vec<String>>();

            Ok(result.concat())
        } else {
            panic!(
                "Fixture {} has incorrect # arguments (expected 2)",
                fixture.file_name
            );
        }
    } else {
        panic!("Fixture {} missing %arguments%", fixture.file_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 std::sync::Arc;

use common::SourceLocationKey;
use common::Span;
use fixture_tests::Fixture;
use graphql_ir::build;
use graphql_ir::reexport::Intern;
use graphql_ir::Program;
use graphql_syntax::parse_executable;
use graphql_test_helpers::diagnostics_to_sorted_string;
use lsp_types::HoverContents;
use lsp_types::MarkedString;
use relay_lsp::hover::get_hover;
use relay_lsp::ContentConsumerType;
use relay_lsp::DummyExtraDataProvider;
use relay_test_schema::get_test_schema;
use relay_test_schema::get_test_schema_with_extensions;
use resolution_path::ResolvePosition;

pub fn transform_fixture(fixture: &Fixture<'_>) -> Result<String, String> {
    let parts: Vec<_> = fixture.content.split("%extension%").collect();
    let (schema, document) = if let [document, extension] = parts.as_slice() {
        (get_test_schema_with_extensions(extension), *document)
    } else {
        (get_test_schema(), fixture.content)
    };
    let cursor_position = document.find('|').unwrap() - 1;
    let source = document.replace("|", "");
    let source_location = SourceLocationKey::standalone(fixture.file_name);
    let ast = parse_executable(&source, source_location)
        .map_err(|diagnostics| diagnostics_to_sorted_string(&source, &diagnostics))?;
    let ir = build(&schema, &ast.definitions)
        .map_err(|diagnostics| diagnostics_to_sorted_string(&source, &diagnostics))?;
    let program = Program::from_definitions(Arc::clone(&schema), ir);

    let path = ast.resolve((), Span::from_usize(cursor_position, cursor_position));

    let schema_name = "Some Schema Name".intern();

    let extra_data_provider = DummyExtraDataProvider::new();

    let hover_contents = get_hover(
        &path,
        &schema,
        schema_name,
        &extra_data_provider,
        &schema,
        &program,
        ContentConsumerType::Relay,
    )
    .ok_or("<NO HOVER RESPONSE>")?
    .contents;

    Ok(print_hover_contents(hover_contents))
}

fn print_hover_contents(contents: HoverContents) -> String {
    match contents {
        HoverContents::Scalar(scalar) => print_marked_string(scalar),
        HoverContents::Array(arr) => arr
            .into_iter()
            .map(print_marked_string)
            .collect::<Vec<_>>()
            .join("\n--\n"),
        HoverContents::Markup(markup) => markup.value,
    }
}

fn print_marked_string(marked_string: MarkedString) -> String {
    match marked_string {
        MarkedString::String(string) => string,
        MarkedString::LanguageString(language_string) => format!(
            "```{}\n{}\n```",
            language_string.language, language_string.value
        ),
    }
}


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

//! This crate contains a GraphQL schema representation.

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

use std::iter::once;

use ::intern::string_key::StringKey;
use common::ArgumentName;
use common::DiagnosticsResult;
use common::DirectiveName;
use common::SourceLocationKey;
use intern::intern;
use lazy_static::lazy_static;
use schema::ArgumentDefinitions;
use schema::SDLSchema;
use schema::TypeReference;

const RELAY_EXTENSIONS: &str = include_str!("./relay-extensions.graphql");

lazy_static! {
    static ref DEFER: DirectiveName = DirectiveName(intern!("defer"));
    static ref STREAM: DirectiveName = DirectiveName(intern!("stream"));
    static ref LABEL: ArgumentName = ArgumentName(intern!("label"));
    pub static ref CUSTOM_SCALAR_DIRECTIVE_NAME: StringKey = intern!("__RelayCustomScalar");
    pub static ref PATH_CUSTOM_SCALAR_ARGUMENT_NAME: StringKey = intern!("path");
    pub static ref EXPORT_NAME_CUSTOM_SCALAR_ARGUMENT_NAME: StringKey = intern!("export_name");
}

pub fn build_schema_with_extensions<T: AsRef<str>, U: AsRef<str>>(
    server_sdls: &[(T, SourceLocationKey)],
    extension_sdls: &[(U, SourceLocationKey)],
) -> DiagnosticsResult<SDLSchema> {
    let extensions: Vec<(&str, SourceLocationKey)> =
        once((RELAY_EXTENSIONS, SourceLocationKey::generated()))
            .chain(
                extension_sdls
                    .iter()
                    .map(|(source, location_key)| (source.as_ref(), *location_key)),
            )
            .collect();

    let mut schema = schema::build_schema_with_extensions(server_sdls, &extensions)?;

    // Remove label arg from @defer and @stream directives since the compiler
    // adds these arguments.
    for directive_name in &[*DEFER, *STREAM] {
        if let Some(directive) = schema.get_directive_mut(*directive_name) {
            let mut next_args: Vec<_> = directive.arguments.iter().cloned().collect();
            for arg in next_args.iter_mut() {
                if arg.name == *LABEL {
                    if let TypeReference::NonNull(of) = &arg.type_ {
                        arg.type_ = *of.clone()
                    };
                }
            }
            directive.arguments = ArgumentDefinitions::new(next_args);
        }
    }
    Ok(schema)
}


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

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

use std::sync::Arc;

use common::SourceLocationKey;
use lazy_static::lazy_static;
use relay_schema::build_schema_with_extensions;
use schema::SDLSchema;

const TEST_SCHEMA_DATA: &str = include_str!("testschema.graphql");
const TEST_SCHEMA_WITH_CUSTOM_ID_DATA: &str = include_str!("testschema_with_custom_id.graphql");

lazy_static! {
    pub static ref TEST_SCHEMA: Arc<SDLSchema> = Arc::new(
        build_schema_with_extensions::<_, &str>(
            &[(TEST_SCHEMA_DATA, SourceLocationKey::generated())],
            &[]
        )
        .expect("Expected test schema to be valid")
    );
    pub static ref TEST_SCHEMA_WITH_CUSTOM_ID: Arc<SDLSchema> = Arc::new(
        build_schema_with_extensions::<_, &str>(
            &[(
                TEST_SCHEMA_WITH_CUSTOM_ID_DATA,
                SourceLocationKey::generated()
            )],
            &[]
        )
        .expect("Expected test schema to be valid")
    );
}

pub fn get_test_schema() -> Arc<SDLSchema> {
    Arc::clone(&TEST_SCHEMA)
}

pub fn get_test_schema_with_extensions(extensions_sdl: &str) -> Arc<SDLSchema> {
    get_test_schema_with_located_extensions(extensions_sdl, SourceLocationKey::generated())
}

pub fn get_test_schema_with_located_extensions(
    extensions_sdl: &str,
    source_location: SourceLocationKey,
) -> Arc<SDLSchema> {
    Arc::new(
        build_schema_with_extensions(
            &[(TEST_SCHEMA_DATA, SourceLocationKey::generated())],
            &[(extensions_sdl, source_location)],
        )
        .expect("Expected test schema (and extensions) to be valid"),
    )
}

pub fn get_test_schema_with_custom_id() -> Arc<SDLSchema> {
    Arc::clone(&TEST_SCHEMA_WITH_CUSTOM_ID)
}

pub fn get_test_schema_with_custom_id_with_extensions(extensions_sdl: &str) -> Arc<SDLSchema> {
    Arc::new(
        build_schema_with_extensions(
            &[(
                TEST_SCHEMA_WITH_CUSTOM_ID_DATA,
                SourceLocationKey::generated(),
            )],
            &[(extensions_sdl, SourceLocationKey::generated())],
        )
        .expect("Expected test schema (and extensions) to be valid"),
    )
}


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

use graphql_ir::Argument;
use graphql_ir::FragmentDefinitionName;
use intern::string_key::Intern;

use super::hash_arguments;

pub fn get_applied_fragment_name(
    spread_name: FragmentDefinitionName,
    args: &[Argument],
) -> FragmentDefinitionName {
    let arguments_hash = hash_arguments(args);
    match arguments_hash {
        Some(hash) => FragmentDefinitionName(format!("{}_{}", spread_name, hash).intern()),
        None => spread_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 common::DiagnosticsResult;
use common::PerfLogEvent;
use graphql_ir::FragmentDefinitionNameSet;
use graphql_ir::Program;
use relay_config::ProjectConfig;

pub type BaseFragmentNames = FragmentDefinitionNameSet;

#[derive(Clone)]
pub struct CustomTransform {
    pub name: &'static str,
    pub transform: fn(&ProjectConfig, &Program, &BaseFragmentNames) -> DiagnosticsResult<Program>,
}

impl std::fmt::Debug for CustomTransform {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Transform")
            .field("name", &self.name)
            .finish()
    }
}

#[derive(Debug)]
pub struct CustomTransforms {
    pub apply_before: Vec<CustomTransform>,
    pub apply_after: Vec<CustomTransform>,
}

#[derive(Debug)]
pub struct CustomTransformsConfig {
    pub common_transforms: Option<CustomTransforms>,
    pub reader_transforms: Option<CustomTransforms>,
    pub operation_transforms: Option<CustomTransforms>,
    pub normalization_transforms: Option<CustomTransforms>,
    pub operation_text_transforms: Option<CustomTransforms>,
    pub typegen_transforms: Option<CustomTransforms>,
}

pub(crate) fn apply_before_custom_transforms(
    program: &Program,
    custom_transforms: &Option<&CustomTransforms>,
    project_config: &ProjectConfig,
    base_fragment_names: &BaseFragmentNames,
    log_event: &impl PerfLogEvent,
    maybe_print_stats: Option<fn(extra_info: &str, program: &Program) -> ()>,
) -> DiagnosticsResult<Program> {
    match custom_transforms {
        Some(CustomTransforms { apply_before, .. }) => apply_custom_transforms(
            program,
            apply_before,
            project_config,
            base_fragment_names,
            log_event,
            maybe_print_stats,
        ),
        _ => Ok(program.clone()),
    }
}

pub(crate) fn apply_after_custom_transforms(
    program: &Program,
    custom_transforms: &Option<&CustomTransforms>,
    project_config: &ProjectConfig,
    base_fragment_names: &BaseFragmentNames,
    log_event: &impl PerfLogEvent,
    maybe_print_stats: Option<fn(extra_info: &str, program: &Program) -> ()>,
) -> DiagnosticsResult<Program> {
    match custom_transforms {
        Some(CustomTransforms { apply_after, .. }) => apply_custom_transforms(
            program,
            apply_after,
            project_config,
            base_fragment_names,
            log_event,
            maybe_print_stats,
        ),
        _ => Ok(program.clone()),
    }
}

fn apply_custom_transforms(
    program: &Program,
    transforms: &[CustomTransform],
    project_config: &ProjectConfig,
    base_fragment_names: &BaseFragmentNames,
    log_event: &impl PerfLogEvent,
    maybe_print_stats: Option<fn(extra_info: &str, program: &Program) -> ()>,
) -> DiagnosticsResult<Program> {
    if transforms.is_empty() {
        return Ok(program.clone());
    }

    let apply = |program: &Program, transform: &CustomTransform| {
        apply_custom_transform(
            program,
            transform,
            project_config,
            base_fragment_names,
            log_event,
            maybe_print_stats,
        )
    };
    let mut next_program = program.clone();
    for custom_transform in transforms.iter() {
        next_program = apply(&next_program, custom_transform)?;
    }

    Ok(next_program)
}

fn apply_custom_transform(
    program: &Program,
    transform: &CustomTransform,
    project_config: &ProjectConfig,
    base_fragment_names: &BaseFragmentNames,
    log_event: &impl PerfLogEvent,
    maybe_print_stats: Option<fn(extra_info: &str, program: &Program) -> ()>,
) -> DiagnosticsResult<Program> {
    let CustomTransform { name, transform } = transform;
    let program = log_event.time(name, || {
        transform(project_config, program, base_fragment_names)
    })?;

    if let Some(print_stats) = maybe_print_stats {
        print_stats(name, &program);
    }
    Ok(program)
}