#![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 napi::bindgen_prelude;
use napi_derive::napi;

use crate::event_loop::Event;

mod event_loop;
mod inline_credit;
mod repository;
mod uri;
mod vscode_sys;

const BUILD_INFO: extension_build_info::Build = extension_build_info::include_build_info!();
pub const PIJUL_SCHEME: &str = "pijul";

#[tracing::instrument(skip_all)]
fn provide_file_decoration<'env>(
    env: &'env napi::Env,
    vscode_uri: vscode_sys::Uri,
    _cancellation_token: bindgen_prelude::Object,
) -> Result<bindgen_prelude::Object<'env>, napi::Error> {
    let uri = uri::from_vscode(&vscode_uri)?;

    let (deferred_promise, promise_object) = env.create_deferred()?;
    event_loop::send(Event::RequestFileDecoration {
        uri,
        deferred_promise,
    });

    Ok(promise_object)
}

#[tracing::instrument(skip_all)]
fn provide_text_document_content<'env>(
    env: &'env napi::Env,
    encoded_uri: vscode_sys::Uri,
    _cancellation_token: bindgen_prelude::Object,
) -> Result<bindgen_prelude::Object<'env>, napi::Error> {
    // Example: pijul:tracked/dnNjb2RlOmV4YW1wbGUvdXJp -> vscode:example/uri
    let decoded_uri = uri::decode(&encoded_uri)?;

    let (deferred_promise, promise_object) = env.create_deferred()?;
    event_loop::send(Event::RequestTrackedContents {
        uri: decoded_uri,
        deferred_promise,
    });

    Ok(promise_object)
}

fn provide_original_resource<'env>(
    env: &'env napi::Env,
    uri_to_encode: vscode_sys::Uri<'env>,
    _cancellation_token: bindgen_prelude::Object,
) -> Result<Option<vscode_sys::Uri<'env>>, napi::Error> {
    // Encode the original URI as part of a Pijul URI
    // Example: vscode:example/uri -> pijul:tracked/dnNjb2RlOmV4YW1wbGUvdXJp
    let encoded_uri = uri::encode(&uri_to_encode)?;
    let vscode_uri = uri::to_vscode(env, &encoded_uri)?;

    Ok(Some(vscode_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`).
    // Since the output view is a text document, any logging calls will trigger this function,
    // so make sure to return before it sends itself into an infinite loop.
    if document_uri.get_scheme()? == "output" {
        return Ok(());
    }

    let uri = uri::from_vscode(&document_uri)?;
    let change_events = event.get_content_changes()?;
    let mut changes = Vec::with_capacity(change_events.len());

    for change_event in &change_events {
        changes.push(event_loop::EditorContentsChange {
            character_offset: change_event.get_range_offset()?,
            characters_replaced: change_event.get_range_length()?,
            replacement_text: change_event.get_text()?,
        })
    }

    event_loop::send(Event::ChangeEditorContents { uri, changes });

    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 vscode_uri = document.get_uri()?;

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

    let uri = uri::from_vscode(&vscode_uri)?;
    event_loop::send(Event::RequestInlineCredit { uri });

    Ok(())
}

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

    for added_workspace in added_workspaces {
        let added_workspace_uri = added_workspace.get_uri()?;
        let added_uri = uri::from_vscode(&added_workspace_uri)?;

        event_loop::send(Event::OpenWorkspaceFolder {
            workspace_uri: added_uri,
        });
    }

    for removed_workspace in removed_workspaces {
        // TODO
    }

    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> {
    for text_editor in &visible_text_editors {
        let document_uri = text_editor.get_document()?.get_uri()?;
        let uri = uri::from_vscode(&document_uri)?;

        event_loop::send(Event::OpenTextEditor {
            uri,
            text_editor: text_editor.create_ref()?,
        });
    }

    Ok(())
}

#[tracing::instrument(skip_all)]
pub fn handle_fs_watcher_event(
    _env: &napi::Env,
    vscode_uri: vscode_sys::Uri,
) -> Result<(), napi::Error> {
    let uri = uri::from_vscode(&vscode_uri)?;
    event_loop::send(Event::ChangedFilesystemContents { uri });

    Ok(())
}

#[tracing::instrument(skip_all)]
pub fn on_did_move_files(
    _env: &napi::Env,
    event: vscode_sys::FileRenameEvent,
) -> Result<(), napi::Error> {
    for renamed_file in event.get_files()? {
        let old_vscode_uri = renamed_file.get_old_uri()?;
        let new_vscode_uri = renamed_file.get_new_uri()?;

        let old_uri = uri::from_vscode(&old_vscode_uri)?;
        let new_uri = uri::from_vscode(&new_vscode_uri)?;

        event_loop::send(Event::MovePath { old_uri, new_uri });
    }

    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 decoration_change_event_emitter = vscode_sys::EventEmitter::new(env)?;
    let decoration_change_event = decoration_change_event_emitter.get_event()?;

    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 mut quick_diff_provider = vscode_sys::QuickDiffProvider::new(env)?;
    quick_diff_provider.set_provide_original_resource(env, provide_original_resource)?;

    event_loop::start(
        env,
        &vscode_object,
        decoration_change_event_emitter,
        quick_diff_provider,
    )?;

    for workspace_folder in vscode_sys::workspace::get_workspace_folders(env)? {
        let vscode_uri = workspace_folder.get_uri()?;
        let workspace_uri = uri::from_vscode(&vscode_uri)?;

        event_loop::send(Event::OpenWorkspaceFolder { workspace_uri });
    }

    let visible_text_editors = vscode_sys::window::get_visible_text_editors(env)?;
    for text_editor in visible_text_editors {
        let document_uri = text_editor.get_document()?.get_uri()?;
        let uri = uri::from_vscode(&document_uri)?;

        event_loop::send(Event::OpenTextEditor {
            uri,
            text_editor: text_editor.create_ref()?,
        });
    }

    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,
    )?;

    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)?;
    vscode_sys::workspace::on_did_rename_files(env, on_did_move_files)?;

    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!(message = "Extension activated", ?BUILD_INFO);

    Ok(())
}