#![feature(map_try_insert)]
#![feature(mapped_lock_guards)]

// TODO: fix credits in quick diff panel
// TODO: find a way to validate all subscriptions are disposed? or just standardize how they are registered
// TODO: replace `get_repository_folder` with something that can handle nested repositories
// TODO: consistent naming
// TODO: handle adding/removing workspace folders (and adding/removing .pijul folders)
// TODO: handle changing active text editors
// TODO: color workspace root with `gitDecoration.submoduleResourceForeground`?
// TODO: something breaks when opening quick diff
// TODO: static handling of package.json contributions
// TODO: move l10n into pijul-extension
// TODO: warn instead of returning an error for non-fatal errors

use std::sync::{Mutex, MutexGuard, OnceLock};

use camino::Utf8PathBuf;
use napi::bindgen_prelude;
use napi_derive::napi;
use pijul_extension::path_state::{PathState, TrackedState};

use repository::OpenRepositories;

mod inline_credit;
mod repository;
mod vscode_sys;

static EXTENSION_STATE: OnceLock<Mutex<ExtensionState>> = OnceLock::new();

pub const PIJUL_SCHEME: &str = "pijul";

struct ExtensionState {
    decoration_type: vscode_sys::reference::TextEditorDecorationTypeRef,
    decoration_change_event_emitter: vscode_sys::reference::EventEmitterRef,
    repositories: OpenRepositories,
}

impl ExtensionState {
    fn get() -> Result<MutexGuard<'static, Self>, napi::Error> {
        EXTENSION_STATE
            .get()
            .ok_or_else(|| napi::Error::from_reason("Extension state is not set"))?
            .lock()
            .map_err(|_error| napi::Error::from_reason("Extension state mutex has been poisoned"))
    }
}

fn provide_file_decoration<'env>(
    env: &'env napi::Env,
    uri: vscode_sys::Uri,
    _cancellation_token: bindgen_prelude::Object,
) -> Result<Option<vscode_sys::FileDecoration<'env>>, napi::Error> {
    let program_state = ExtensionState::get()?;

    let Some((repository_path, open_repository)) =
        program_state.repositories.get_open_repository(env, &uri)?
    else {
        return Ok(None);
    };

    let absolute_file_path = Utf8PathBuf::from(uri.get_fs_path()?);
    let relative_file_path =
        absolute_file_path
            .strip_prefix(&repository_path)
            .map_err(|error| {
                napi::Error::from_reason(format!(
                    "Failed to strip prefix {repository_path} from {absolute_file_path}: {error}"
                ))
            })?;

    let Some(path_state) = open_repository
        .repository
        .get_path_state(relative_file_path)
    else {
        return Ok(None);
    };

    // TODO: l10n_embed
    let (badge, tooltip, color_id) = match path_state {
        PathState::Untracked => ("U", "Untracked", "pijul.decorations.path.untracked"),
        PathState::Tracked(modified_state) => match modified_state {
            TrackedState::Added => ("A", "Added", "pijul.decorations.path.added"),
            TrackedState::Removed => ("RM", "Removed", "pijul.decorations.path.removed"),
            TrackedState::Modified => ("M", "Modified", "pijul.decorations.path.modified"),
            TrackedState::Moved => ("MV", "Moved", "pijul.decorations.path.moved"),
            TrackedState::ModifiedAndMoved => (
                "M,MV",
                "Modified, Moved",
                "pijul.decorations.path.modifiedAndMoved",
            ),
        },
    };
    let color = vscode_sys::ThemeColor::new(env, color_id)?;

    let path_decoration = vscode_sys::FileDecoration::new(env, badge, tooltip, &color)?;
    Ok(Some(path_decoration))
}

fn provide_text_document_content(
    env: &napi::Env,
    pijul_uri: vscode_sys::Uri,
    _cancellation_token: bindgen_prelude::Object,
) -> Result<Option<String>, napi::Error> {
    let program_state = ExtensionState::get()?;

    let uri_change = vscode_sys::UriWithChange::new(env)?.scheme("file")?;
    let uri = pijul_uri.with(uri_change)?;

    if let Some((workspace_path, workspace)) =
        program_state.repositories.get_open_repository(env, &uri)?
    {
        let absolute_path = Utf8PathBuf::from(uri.get_fs_path()?);
        let relative_path = absolute_path
            .strip_prefix(&workspace_path)
            .map_err(|error| {
                napi::Error::from_reason(format!(
                    "Failed to strip prefix {workspace_path} from {absolute_path}: {error}"
                ))
            })?;

        match workspace.repository.get_open_file(relative_path) {
            Some(open_file) => Ok(open_file.tracked_contents().map(str::to_string)),
            None => {
                tracing::error!(
                    message = "No tracked file state for {relative_path}",
                    ?workspace_path
                );

                Ok(None)
            }
        }
    } else {
        tracing::debug!(message = "Ignoring URI", uri = uri.to_string()?);

        Ok(None)
    }
}

fn provide_original_resource<'env>(
    env: &'env napi::Env,
    uri: vscode_sys::Uri<'env>,
    _cancellation_token: bindgen_prelude::Object,
) -> Result<Option<vscode_sys::Uri<'env>>, napi::Error> {
    // TODO: create a proper uri that preserves everything, including the original scheme
    let uri_change = vscode_sys::UriWithChange::new(env)?.scheme(PIJUL_SCHEME)?;
    let pijul_uri = uri.with(uri_change)?;

    Ok(Some(pijul_uri))
}

#[tracing::instrument(skip_all)]
fn on_did_change_text_document(
    env: &napi::Env,
    event: vscode_sys::TextDocumentChangeEvent,
) -> Result<(), napi::Error> {
    let text_document = event.get_document()?;
    let document_uri = text_document.get_uri()?;

    // Ignore any messages printed to the `Output` channel (used by `tracing`)
    if document_uri.get_scheme()? == "output" {
        return Ok(());
    }

    let mut program_state = ExtensionState::get()?;

    if let Some((repository_path, open_repository)) = program_state
        .repositories
        .get_open_repository_mut(env, &document_uri)?
    {
        let absolute_document_path = Utf8PathBuf::from(document_uri.get_fs_path()?);
        tracing::debug!(?absolute_document_path);

        let relative_document_path = absolute_document_path
            .strip_prefix(&repository_path)
            .map_err(|error| napi::Error::from_reason(format!("Failed to strip prefix {repository_path} from {absolute_document_path}: {error}")))?;

        let change_events = event.get_content_changes()?;

        for change_event in change_events {
            let character_offset = change_event.get_range_offset()?;
            let characters_replaced = change_event.get_range_length()?;
            let replacement_text = change_event.get_text()?;

            open_repository
                .repository
                .update_open_file(
                    relative_document_path,
                    character_offset as usize,
                    characters_replaced as usize,
                    &replacement_text,
                )
                .map_err(|error| {
                    napi::Error::from_reason(format!(
                        "Unable to update open file {relative_document_path} ({absolute_document_path}): {error}"
                    ))
                })?;
        }

        // Re-render the inline credit annotation
        let text_editor = open_repository
            .get_text_editor(env, relative_document_path)?
            .ok_or_else(|| {
                napi::Error::from_reason(format!(
                    "no open text editor for {absolute_document_path}"
                ))
            })?;

        inline_credit::render(env, &program_state, &text_editor)?;
    }

    Ok(())
}

#[tracing::instrument(skip_all)]
fn on_did_change_text_editor_selections(
    env: &napi::Env,
    event: vscode_sys::TextEditorSelectionChangeEvent,
) -> Result<(), napi::Error> {
    let editor = event.get_text_editor()?;
    let document = editor.get_document()?;
    let uri = document.get_uri()?;

    // Ignore any messages printed to the `Output` channel (used by `tracing`)
    if uri.get_scheme()? == "output" {
        return Ok(());
    }

    let program_state = ExtensionState::get()?;
    inline_credit::render(env, &program_state, &editor)
}

#[tracing::instrument(skip_all)]
fn on_did_change_workspace_folders(
    env: &napi::Env,
    event: vscode_sys::WorkspaceFoldersChangeEvent,
) -> Result<(), napi::Error> {
    let mut program_state = ExtensionState::get()?;
    let added_workspaces = event.get_added()?;
    let removed_workspaces = event.get_removed()?;

    for added_workspace in added_workspaces {
        program_state
            .repositories
            .open_workspace_folder(env, added_workspace)?;
    }

    for removed_workspace in removed_workspaces {
        program_state
            .repositories
            .close_workspace_folder(removed_workspace)?;
    }

    Ok(())
}

// TODO: handle closing text editors
#[tracing::instrument(skip_all)]
fn on_did_change_visible_text_editors(
    env: &napi::Env,
    visible_text_editors: Vec<vscode_sys::TextEditor>,
) -> Result<(), napi::Error> {
    let mut extension_state = ExtensionState::get()?;

    extension_state
        .repositories
        .register_text_editors(env, visible_text_editors)
}

#[tracing::instrument(skip_all)]
pub fn handle_fs_watcher_event(env: &napi::Env, uri: vscode_sys::Uri) -> Result<(), napi::Error> {
    let mut extension_state = crate::ExtensionState::get()?;
    let decoration_change_event_emitter = extension_state
        .decoration_change_event_emitter
        .get_inner(env)?;

    if let Some((repository_path, open_repository)) = extension_state
        .repositories
        .get_open_repository_mut(env, &uri)?
    {
        let absolute_file_path = Utf8PathBuf::from(uri.get_fs_path()?);
        let relative_file_path = absolute_file_path
            .strip_prefix(&repository_path)
            .map_err(|error| {
                napi::Error::from_reason(format!(
                    "Failed to strip prefix {repository_path} from {absolute_file_path}: {error}"
                ))
            })?
            .to_path_buf();

        open_repository
            .repository
            .update_path_state(relative_file_path)
            .map_err(|error| {
                napi::Error::from_reason(format!("Failed to update path state: {error:?}"))
            })?;
        decoration_change_event_emitter.fire(uri.inner)?;
        open_repository.update_resource_states(env, &repository_path)?;
    }

    Ok(())
}

// TODO: make sure things are registered in order:
// 1. Create extension state
// 2. First-time init e.g. initial inline credit and open files
// 3. Register extension state
// 4. Providers
// 5. Event handlers
// TODO: remove catch_unwind (make activate_internal)
#[napi(catch_unwind)]
pub fn activate(
    env: &napi::Env,
    vscode_object: bindgen_prelude::Object,
    extension_context: bindgen_prelude::Object,
) -> Result<(), napi::Error> {
    vscode_sys::activate(&vscode_object, &extension_context)?;
    vscode_sys::log::init(
        env,
        "Pijul",
        tracing_subscriber::fmt::format::DefaultFields::new(),
    )?;

    let mut repositories = OpenRepositories::new();

    for workspace_folder in vscode_sys::workspace::get_workspace_folders(env)? {
        repositories.open_workspace_folder(env, workspace_folder)?;
    }

    let visible_text_editors = vscode_sys::window::get_visible_text_editors(env)?;
    repositories.register_text_editors(env, visible_text_editors)?;

    let decoration_type = inline_credit::create_decoration_type(env)?;

    let decoration_change_event_emitter = vscode_sys::EventEmitter::new(env)?;
    let decoration_change_event = decoration_change_event_emitter.get_event()?;

    let extension_state = ExtensionState {
        decoration_type: decoration_type.create_ref()?,
        decoration_change_event_emitter: decoration_change_event_emitter.create_ref()?,
        repositories,
    };

    if let Some(active_text_editor) = vscode_sys::window::get_active_text_editor(env)? {
        inline_credit::render(env, &extension_state, &active_text_editor)?;
    }

    EXTENSION_STATE
        .set(Mutex::new(extension_state))
        .map_err(|_existing_object| {
            napi::Error::from_reason("Extension state has already been set")
        })?;

    let mut file_decoration_provider =
        vscode_sys::FileDecorationProvider::new(env, provide_file_decoration)?;
    file_decoration_provider.set_on_did_change_file_decorations(decoration_change_event)?;
    vscode_sys::window::register_file_decoration_provider(
        env,
        &extension_context,
        file_decoration_provider,
    )?;

    let text_document_provider =
        vscode_sys::TextDocumentContentProvider::new(env, provide_text_document_content)?;
    vscode_sys::workspace::register_text_document_content_provider(
        env,
        &extension_context,
        PIJUL_SCHEME,
        text_document_provider,
    )?;

    let mut quick_diff_provider = vscode_sys::QuickDiffProvider::new(env)?;
    quick_diff_provider.set_provide_original_resource(env, provide_original_resource)?;

    let program_state = ExtensionState::get()?;
    for (_repository_path, open_repository) in program_state.repositories.iter_repositories() {
        let mut source_control = open_repository.source_control.get_inner(env)?;

        source_control.set_quick_diff_provider(quick_diff_provider)?;
    }

    vscode_sys::window::on_did_change_text_editor_selections(
        env,
        on_did_change_text_editor_selections,
    )?;
    vscode_sys::window::on_did_change_visible_text_editors(
        env,
        on_did_change_visible_text_editors,
    )?;

    vscode_sys::workspace::on_did_change_text_document(env, on_did_change_text_document)?;
    vscode_sys::workspace::on_did_change_workspace_folders(env, on_did_change_workspace_folders)?;

    let file_system_watcher = vscode_sys::workspace::create_file_system_watcher(env, "**")?;
    file_system_watcher.on_did_change(env, handle_fs_watcher_event)?;
    file_system_watcher.on_did_create(env, handle_fs_watcher_event)?;
    file_system_watcher.on_did_delete(env, handle_fs_watcher_event)?;

    tracing::info!("Extension activated");

    Ok(())
}