Documentation
use std::panic;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

use anyhow::{bail, Result};
use crossbeam_channel::{select, Receiver, Sender};
use liwe::model::config::Configuration;
use liwe::model::State;
use log::{debug, error};
use lsp_server::{ErrorCode, Message, Request};
use lsp_server::{Notification, Response};
use lsp_types::{
    CodeAction, CodeActionParams, CompletionItem, DidChangeTextDocumentParams,
    DidChangeWatchedFilesParams, DidSaveTextDocumentParams, DocumentFormattingParams,
    DocumentSymbolParams, FoldingRangeParams, GotoDefinitionResponse, HoverParams,
    InlayHintParams, InlineValueParams, ReferenceParams, RenameParams, ShowDocumentParams,
    TextDocumentPositionParams, WorkspaceSymbolParams,
};
use lsp_types::{CompletionParams, GotoDefinitionParams};

use self::server::DefinitionResult;
use serde::Deserialize;
use serde_json::to_value;
use uuid::Uuid;

use self::server::Server;

pub mod server;

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum LspClient {
    Unknown,
    Helix,
}

pub struct ServerConfig {
    pub base_path: String,
    pub state: State,
    pub sequential_ids: Option<bool>,
    pub configuration: Configuration,
    pub lsp_client: LspClient,
    pub override_now: Option<std::time::SystemTime>,
}

#[derive(Clone)]
pub struct Router {
    server: Arc<Server>,
    sender: Sender<Message>,
    inlay_hints_used: Arc<AtomicBool>,
}

impl Router {
    pub fn respond(&self, response: Response) {
        self.send(response.into());
    }

    fn send(&self, message: Message) {
        if let Err(e) = self.sender.send(message) {
            error!("Failed to send LSP message: {}", e);
        }
    }

    fn delay_send(&self, message: Message) {
        let sender = self.sender.clone();
        std::thread::spawn(move || {
            std::thread::sleep(std::time::Duration::from_millis(100));
            if let Err(e) = sender.send(message) {
                log::error!("Failed to send delayed LSP message: {}", e);
            }
        });
    }

    pub fn new(sender: Sender<Message>, config: ServerConfig) -> Self {
        debug!(
            "initializing LSP database at {}, with {} docs",
            config.base_path,
            config.state.len()
        );

        let router = Self {
            server: Arc::new(Server::new(config)),
            sender,
            inlay_hints_used: Arc::new(AtomicBool::new(false)),
        };

        debug!("initializing LSP database complete");

        router
    }

    fn next_event(&self, inbox: &Receiver<Message>) -> Option<Message> {
        select! {
            recv(inbox) -> msg =>
                msg.ok()
        }
    }

    pub fn run(mut self, receiver: Receiver<Message>) -> Result<()> {
        use std::panic::AssertUnwindSafe;

        while let Some(message) = self.next_event(&receiver) {
            let shutdown = panic::catch_unwind(AssertUnwindSafe(|| self.handle_message(message)))
                .unwrap_or_else(|err| {
                    let error_message = if let Some(string) = err.downcast_ref::<&str>() {
                        format!("Panic occurred with message: {}", string)
                    } else if let Some(string) = err.downcast_ref::<String>() {
                        format!("Panic occurred with message: {}", string)
                    } else {
                        "Panic occurred with unknown cause".to_string()
                    };
                    error!("Panic message: {}", error_message);
                    false
                });

            if shutdown {
                return Ok(());
            }
        }
        bail!("client exited without proper shutdown sequence")
    }

    fn handle_message(&mut self, message: Message) -> bool {
        match message {
            Message::Request(req) => self.on_request(req),
            Message::Notification(notification) => self.on_notification(notification),
            Message::Response(_) => false,
        }
    }

    fn on_notification(&mut self, notification: Notification) -> bool {
        if notification.method == "exit" {
            return true;
        }

        match notification.method.as_str() {
            "textDocument/didChange" => {
                match DidChangeTextDocumentParams::deserialize(notification.params) {
                    Ok(params) => {
                        if let Some(server) = Arc::get_mut(&mut self.server) {
                            server.handle_did_change_text_document(params);
                        } else {
                            error!("Failed to get mutable reference to server");
                        }
                    }
                    Err(e) => error!("Failed to deserialize didChange params: {}", e),
                }
            }
            "textDocument/didSave" => {
                match DidSaveTextDocumentParams::deserialize(notification.params) {
                    Ok(params) => {
                        if let Some(server) = Arc::get_mut(&mut self.server) {
                            server.handle_did_save_text_document(params);
                        } else {
                            error!("Failed to get mutable reference to server");
                        }
                    }
                    Err(e) => error!("Failed to deserialize didSave params: {}", e),
                }
            }
            "workspace/didChangeWatchedFiles" => {
                match DidChangeWatchedFilesParams::deserialize(notification.params) {
                    Ok(params) => {
                        if let Some(server) = Arc::get_mut(&mut self.server) {
                            server.handle_did_change_watched_files(params);
                        } else {
                            error!("Failed to get mutable reference to server");
                        }
                    }
                    Err(e) => error!("Failed to deserialize didChangeWatchedFiles params: {}", e),
                }
            }
            default => {
                debug!("unhandled request: {}", default)
            }
        };

        false
    }

    fn on_request(&self, request: Request) -> bool {
        if request.method == "shutdown" {
            self.respond(Response {
                id: request.id.clone(),
                result: Some(serde_json::Value::Null),
                error: None,
            });

            return true;
        }

        let response = match request.method.as_str() {
            "textDocument/inlayHint" => InlayHintParams::deserialize(request.params)
                .map(|params| {
                    self.inlay_hints_used.store(true, Ordering::Relaxed);
                    self.server.handle_inlay_hints(params)
                })
                .map(|response| to_value(response).unwrap()),
            "textDocument/inlineValues" => InlineValueParams::deserialize(request.params)
                .map(|params| self.server.handle_inline_values(params))
                .map(|response| to_value(response).unwrap()),
            "textDocument/documentSymbol" => DocumentSymbolParams::deserialize(request.params)
                .map(|params| self.server.handle_document_symbols(params))
                .map(|response| to_value(response).unwrap()),
            "textDocument/definition" => {
                match GotoDefinitionParams::deserialize(request.params) {
                    Ok(params) => {
                        match self.server.handle_goto_definition(params) {
                            DefinitionResult::Internal(response) => Ok(to_value(response).unwrap()),
                            DefinitionResult::External(url) => {
                                if let Ok(uri) = url.parse() {
                                    let show_doc_request = Request {
                                        id: Uuid::new_v4().to_string().into(),
                                        method: "window/showDocument".to_string(),
                                        params: to_value(ShowDocumentParams {
                                            uri,
                                            external: Some(true),
                                            take_focus: Some(true),
                                            selection: None,
                                        })
                                        .unwrap(),
                                    };
                                    self.send(Message::Request(show_doc_request));
                                }
                                Ok(to_value(GotoDefinitionResponse::Array(vec![])).unwrap())
                            }
                        }
                    }
                    Err(e) => Err(e),
                }
            }
            "workspace/symbol" => WorkspaceSymbolParams::deserialize(request.params)
                .map(|params| self.server.handle_workspace_symbols(params))
                .map(|response| to_value(response).unwrap()),
            "textDocument/hover" => HoverParams::deserialize(request.params)
                .map(|params| self.server.handle_hover(params))
                .map(|response| to_value(response).unwrap()),
            "textDocument/completion" => CompletionParams::deserialize(request.params)
                .map(|params| self.server.handle_completion(params))
                .map(|response| to_value(response).unwrap()),
            "completionItem/resolve" => CompletionItem::deserialize(request.params)
                .map(|params| self.server.resolve_completion(params))
                .map(|response| to_value(response).unwrap()),
            "textDocument/codeAction" => CodeActionParams::deserialize(request.params)
                .map(|params| self.server.handle_code_action(&params))
                .map(|response| to_value(response).unwrap()),
            "codeAction/resolve" => CodeAction::deserialize(request.params)
                .map(|params| self.server.handle_code_action_resolve(&params))
                .map(|response| to_value(response).unwrap()),
            "textDocument/formatting" => DocumentFormattingParams::deserialize(request.params)
                .map(|params| self.server.handle_document_formatting(params))
                .map(|response| to_value(response).unwrap()),
            "textDocument/references" => ReferenceParams::deserialize(request.params)
                .map(|params| self.server.handle_references(params))
                .map(|response| to_value(response).unwrap()),
            "textDocument/prepareRename" => TextDocumentPositionParams::deserialize(request.params)
                .map(|params| self.server.handle_prepare_rename(params))
                .map(|response| to_value(response).unwrap()),
            "textDocument/rename" => RenameParams::deserialize(request.params).map(|params| {
                match self.server.handle_rename(params) {
                    Ok(response) => to_value(response).unwrap(),
                    Err(err) => to_value(err).unwrap(),
                }
            }),
            "textDocument/foldingRange" => FoldingRangeParams::deserialize(request.params)
                .map(|params| self.server.handle_folding_range(params))
                .map(|response| to_value(response).unwrap()),
            default => {
                panic!("unhandled request: {}", default)
            }
        };

        match response {
            Ok(value) => {
                self.respond(Response {
                    id: request.id,
                    result: Some(value),
                    error: None,
                });
                if request.method.as_str() == "codeAction/resolve"
                    && self.inlay_hints_used.load(Ordering::Relaxed)
                {
                    self.delay_send(Message::Request(Request {
                        id: Uuid::new_v4().to_string().into(),
                        method: "workspace/inlayHint/refresh".to_string(),
                        params: serde_json::Value::Null,
                    }));
                }
            }
            Err(_) => self.respond(Response::new_err(
                request.id,
                ErrorCode::InternalError as i32,
                "error handling request".to_string(),
            )),
        }

        false
    }
}