// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
//
// cspell:ignore unwatch
use std::{
    pin::Pin,
    rc::Rc,
    sync::{Arc, atomic},
    task::{Poll, Waker},
    time::Duration,
};

use i_slint_live_preview::file_watcher::{FileWatcher, WatchEvent};
use i_slint_live_preview::protocol::{
    LspToPreviewMessage, PreviewComponent, PreviewToLspMessage, SourceFileVersion, VersionedUrl,
};
use lsp_server::{Message, RequestId};
use lsp_types::{MessageType, Url, notification::Notification};

use crate::{
    common::{self, LspToPreviews, Result, document_cache::OpenImportCallback},
    language, preview,
    preview::connector::EmbeddedLspToPreview,
};

pub fn editor_main() -> std::result::Result<(), slint::PlatformError> {
    tracing_subscriber::fmt()
        .with_writer(std::io::stderr)
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    use clap::Parser;

    let cli = Cli::parse();

    let (to_lsp, from_preview) = crossbeam_channel::unbounded();
    let (to_preview, from_lsp) = crossbeam_channel::unbounded();
    let request_queue = OutgoingRequestQueue::default();

    // TODO: Remove the ServerNotifier, we want to keep the "LSP" abstraction
    // as much out of the visual editor as possible.
    let notifier = ServerNotifier { sender: to_preview, queue: request_queue };

    let to_preview = EmbeddedLspToPreview::new(notifier.clone());

    let to_lsp =
        Rc::new(EmbeddedPreviewToLsp { sender: to_lsp }) as Rc<dyn common::PreviewToLsp + 'static>;

    // Set up the Slint backend (installing the macOS unified-title-bar hook)
    // *before* spawning the LSP thread, so that no other thread can lazily
    // initialize the default platform first and lose the hook.
    start_processing_lsp_messages_thread(from_lsp)?;

    start_lsp_thread(from_preview, to_preview, notifier, cli);

    preview::run(to_lsp, false, true)
}

// TODO: Deduplicate with main.rs
pub enum OutgoingRequest {
    Start,
    Pending(Waker),
    Done(lsp_server::Response),
}

// TODO: Deduplicate with main.rs
pub type OutgoingRequestQueue = Arc<dashmap::DashMap<RequestId, OutgoingRequest>>;

// TODO: Deduplicate with main.rs
/// A handle that can be used to communicate with the client
///
/// This type is duplicated, with the same interface, in main.rs and wasm_main.rs
#[derive(Clone)]
pub struct ServerNotifier {
    sender: crossbeam_channel::Sender<Message>,
    queue: OutgoingRequestQueue,
}

impl ServerNotifier {
    pub fn send_notification<N: Notification>(&self, params: N::Params) -> Result<()> {
        self.sender.send(Message::Notification(lsp_server::Notification::new(
            N::METHOD.to_string(),
            params,
        )))?;
        Ok(())
    }

    pub fn send_request<T: lsp_types::request::Request>(
        &self,
        request: T::Params,
    ) -> Result<impl Future<Output = Result<T::Result>>> {
        static REQ_ID: atomic::AtomicI32 = atomic::AtomicI32::new(0);
        let id = RequestId::from(REQ_ID.fetch_add(1, atomic::Ordering::Relaxed));
        let msg =
            Message::Request(lsp_server::Request::new(id.clone(), T::METHOD.to_string(), request));
        self.sender.send(msg)?;
        let queue = self.queue.clone();
        queue.insert(id.clone(), OutgoingRequest::Start);
        Ok(std::future::poll_fn(move |ctx| match queue.remove(&id).unwrap().1 {
            OutgoingRequest::Pending(_) | OutgoingRequest::Start => {
                queue.insert(id.clone(), OutgoingRequest::Pending(ctx.waker().clone()));
                Poll::Pending
            }
            OutgoingRequest::Done(d) => match d.response_result {
                Err(err) => Poll::Ready(Err(err.message.into())),
                Ok(result) => Poll::Ready(
                    serde_json::from_value(result)
                        .map_err(|e| format!("cannot deserialize response: {e:?}").into()),
                ),
            },
        }))
    }

    #[cfg(test)]
    pub fn dummy() -> Self {
        Self { sender: crossbeam_channel::unbounded().0, queue: Default::default() }
    }
}

struct EmbeddedPreviewToLsp {
    sender: crossbeam_channel::Sender<PreviewToLspMessage>,
}

impl common::PreviewToLsp for EmbeddedPreviewToLsp {
    fn send(&self, message: &PreviewToLspMessage) -> common::Result<()> {
        self.sender.send(message.clone())?;
        Ok(())
    }
}

#[derive(clap::Parser)]
struct Cli {
    file: String,
    component: Option<String>,
}

fn start_processing_lsp_messages_thread(
    from_lsp: crossbeam_channel::Receiver<Message>,
) -> std::result::Result<(), slint::PlatformError> {
    // Ensure the backend is set up before the reader thread starts. This fixes
    // bug #10274 on macOS where a race condition was causing the reader thread to already
    // process messages before the event loop was running.
    let selector = slint::BackendSelector::new();
    // On macOS, request a unified title bar: the editor content extends underneath
    // a transparent title bar (see `preview::macos_titlebar`).
    #[cfg(target_os = "macos")]
    let selector = selector
        .with_winit_window_attributes_hook(crate::preview::macos_titlebar::apply_unified_titlebar);
    selector.select()?;
    std::thread::spawn(move || {
        if let Err(err) = process_lsp_messages(from_lsp) {
            tracing::error!("LSP message processing thread exited with error: {err}");
        }
    });
    Ok(())
}

fn process_lsp_messages(from_lsp: crossbeam_channel::Receiver<Message>) -> common::Result<()> {
    while let Ok(msg) = from_lsp.recv() {
        match msg {
            Message::Notification(notification) => {
                if notification.method == LspToPreviewMessage::METHOD {
                    // TODO: Error handling!
                    let message: LspToPreviewMessage = serde_json::from_value(notification.params)?;

                    slint::invoke_from_event_loop(move || {
                        preview::connector::lsp_to_preview(message);
                    })
                    .map_err(|err| {
                        let err = err.to_string();
                        tracing::error!("Failed to queue message onto event loop - reader thread will exit: {err}");
                        err
                    })?;
                } else {
                    tracing::debug!("Silently ignoring notification from LSP: {:?}", notification);
                }
            }
            msg => {
                tracing::debug!("Silently ignoring message from LSP: {:?}", msg);
            }
        }
    }
    tracing::debug!("LSP->Preview channel closed, quitting reader thread");
    Ok(())
}

fn start_lsp_thread(
    from_preview: crossbeam_channel::Receiver<PreviewToLspMessage>,
    to_preview: EmbeddedLspToPreview,
    notifier: ServerNotifier,
    cli: Cli,
) {
    std::thread::spawn(move || {
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_io()
            .enable_time()
            .build()
            .unwrap();
        let local_set = tokio::task::LocalSet::new();
        if let Err(err) = local_set.block_on(&rt, lsp_main(from_preview, to_preview, notifier, cli))
        {
            tracing::error!("{err}");
            std::process::exit(1);
        }
    });
}

fn bridge_crossbeam_to_tokio(
    from_preview: crossbeam_channel::Receiver<PreviewToLspMessage>,
) -> tokio::sync::mpsc::UnboundedReceiver<PreviewToLspMessage> {
    let (from_preview_tx, from_preview_rx) =
        tokio::sync::mpsc::unbounded_channel::<PreviewToLspMessage>();
    std::thread::spawn(move || {
        while let Ok(msg) = from_preview.recv() {
            if from_preview_tx.send(msg).is_err() {
                break;
            }
        }
        tracing::debug!("Preview->LSP crossbeam adapter thread exited");
    });
    from_preview_rx
}

async fn lsp_main(
    from_preview: crossbeam_channel::Receiver<PreviewToLspMessage>,
    to_preview: EmbeddedLspToPreview,
    notifier: ServerNotifier,
    cli: Cli,
) -> Result<()> {
    use crate::common::document_cache::CompilerConfiguration;

    let mut from_preview_rx = bridge_crossbeam_to_tokio(from_preview);
    let (file_watcher_tx, mut file_watcher_rx) = tokio::sync::mpsc::unbounded_channel();
    let mut file_watcher = FileWatcher::start(
        move |event| {
            if file_watcher_tx.send(event).is_err() {
                tracing::debug!("Ignoring file watcher event after editor shutdown");
            }
        },
        move |err| tracing::warn!("File watcher error: {err}"),
    )?;

    // Wrap to_preview in Rc for sharing with the import callback and Context
    let to_preview = LspToPreviews::with_one(to_preview);

    let open_import_callback = {
        let to_preview = Rc::clone(&to_preview);
        Rc::new(move |path: String| {
            let to_preview = Rc::clone(&to_preview);
            Box::pin(async move {
                tracing::trace!("Importing file: {}", path);
                let contents = std::fs::read(&path);
                if let Ok(url) = Url::from_file_path(&path) {
                    if let Ok(contents) = &contents {
                        to_preview.send(&LspToPreviewMessage::SetContents {
                            url: VersionedUrl::new(url, None),
                            contents: contents.clone(),
                        });
                    } else {
                        to_preview.send(&LspToPreviewMessage::ForgetFile { url });
                    }
                }
                Some(
                    contents
                        .and_then(|c| String::from_utf8(c).map_err(std::io::Error::other))
                        .map(|c| (None, c)),
                )
            })
                as Pin<
                    Box<dyn Future<Output = Option<std::io::Result<(SourceFileVersion, String)>>>>,
                >
        }) as OpenImportCallback
    };
    let compiler_config = CompilerConfiguration {
        style: Some("fluent".into()),
        open_import_callback: Some(open_import_callback),
        format: common::ByteFormat::Utf8,
        ..Default::default()
    };

    let mut ctx = language::Context {
        document_cache: common::DocumentCache::new(compiler_config),
        preview_config: Default::default(),
        server_notifier: notifier,
        init_param: Default::default(),
        #[cfg(any(feature = "preview-external", feature = "preview-engine"))]
        to_show: Default::default(),
        open_urls: Default::default(),
        to_preview,
        pending_recompile: Default::default(),
        host_language_rename_dont_ask_again: Default::default(),
    };

    // Load the initial document through the compiler. This triggers the import
    // callback for all transitive dependencies, sending their contents to the preview.
    let full_path = std::fs::canonicalize(&cli.file)
        .map_err(|err| format!("Failed to determine full path for {}: {err}", cli.file))?;
    let root_path = full_path.clone();
    let url = Url::from_file_path(full_path)
        .map_err(|_| format!("Failed to convert {} to URL!", cli.file))?;
    language::show_preview(
        PreviewComponent { url: url.clone(), component: cli.component },
        &mut ctx,
    );

    // Make sure the document is loaded before we start processing messages from the preview, so we
    // have the correct state already loaded.
    language::reload_document(&mut ctx, url)
        .await
        .map_err(|err| format!("Failed to load file: {}: {err}", cli.file))?;
    let mut watch_paths_revision = None;
    sync_file_watcher_if_needed(&mut file_watcher, &ctx, &root_path, &mut watch_paths_revision)?;

    const RECOMPILE_IDLE_TIMEOUT: Duration = Duration::from_millis(50);
    loop {
        let recompile_idle_timeout =
            if ctx.pending_recompile.is_empty() { Duration::MAX } else { RECOMPILE_IDLE_TIMEOUT };
        tokio::select! {
            watcher_event = file_watcher_rx.recv() => {
                match watcher_event {
                    Some(event) => trigger_editor_file_watcher(&mut ctx, event).await?,
                    None => break Err("File watcher channel closed".into()),
                }
            }
            msg = from_preview_rx.recv() => {
                match msg {
                    Some(msg) => handle_preview_message(msg, &ctx),
                    None => {
                        tracing::debug!("Preview->LSP channel closed, exiting");
                        break Ok(());
                    }
                }
            }
            _ = tokio::time::sleep(recompile_idle_timeout) => {
                tracing::debug!("LSP recompiling");
                let pending_recompile = std::mem::take(&mut ctx.pending_recompile);

                for url in pending_recompile {
                    if let Err(err) = language::reload_document(&mut ctx, url).await {
                        tracing::error!("Failed document reload: {err}");
                    }
                }
            }
        }

        sync_file_watcher_if_needed(
            &mut file_watcher,
            &ctx,
            &root_path,
            &mut watch_paths_revision,
        )?;
    }
}

async fn trigger_editor_file_watcher(
    ctx: &mut language::Context,
    WatchEvent { path, kind }: WatchEvent,
) -> Result<()> {
    let Ok(url) = Url::from_file_path(&path) else {
        tracing::debug!("Ignoring file watcher event for non-file path: {}", path.display());
        return Ok(());
    };

    language::trigger_file_watcher(ctx, url, kind).await
}

fn sync_file_watcher_if_needed(
    watcher: &mut FileWatcher,
    ctx: &language::Context,
    root_path: &std::path::Path,
    watch_paths_revision: &mut Option<u64>,
) -> Result<()> {
    let current_revision = ctx.document_cache.revision();
    if watch_paths_revision.is_some_and(|rev| rev == current_revision) {
        return Ok(());
    }

    watcher.update_watched_paths(
        std::iter::once(root_path.to_path_buf()).chain(
            ctx.document_cache
                .all_urls_to_watch()
                .into_iter()
                // filter out builtins
                .filter(|url| url.scheme() == "file")
                .filter_map(|url| common::uri_to_file(&url)),
        ),
    )?;
    *watch_paths_revision = Some(current_revision);
    Ok(())
}

fn handle_preview_message(msg: PreviewToLspMessage, ctx: &language::Context) {
    use PreviewToLspMessage::*;
    match &msg {
        RequestState { files } => {
            if files.is_empty() {
                tracing::debug!("Preview requested state, re-sending all documents");
                language::send_state_to_preview(ctx);
            } else {
                language::send_files_to_preview(ctx, files);
            }
        }
        SendShowMessage { message } => {
            match message.typ {
                MessageType::ERROR => tracing::error!("Preview: {}", message.message),
                MessageType::WARNING => tracing::warn!("Preview: {}", message.message),
                MessageType::LOG => tracing::debug!("Preview: {}", message.message),
                _ => tracing::info!("Preview: {}", message.message),
            };
        }
        DebugMessage { location, message } => {
            eprintln!("{}", common::preview_log_message_to_string(location, message));
        }

        Diagnostics { .. }
        | ShowDocument { .. }
        | PreviewTypeChanged { .. }
        | TelemetryEvent(..)
        | ConnectRemote { .. }
        | DisconnectRemote
        | Pong => {
            tracing::debug!("Ignoring message from preview: {msg:?}");
        }
        SendWorkspaceEdit { .. } => {
            tracing::warn!("Workspace edits not yet implemented in visual editor");
        }
    }
}
