// TODO: `FromNapiValue` should also validate
// TODO: either completely remove/embrace `FnArgs`

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

use napi::JsValue;
use napi::bindgen_prelude;
use napi::bindgen_prelude::{
    FromNapiValue, JsObjectValue, ToNapiValue, TypeName, ValidateNapiValue,
};
use napi_sys::{napi_env, napi_value};

use macros::{
    class_wrapper, class_wrapper_empty, empty_function_caller, field_getter, field_setter,
    function_caller, function_setter, interface_builder, object_event_handler, static_class_method,
};

// Internal/helper modules
pub mod log;
mod macros;
pub mod reference;

// VSCode API namespaces
pub mod scm;
pub mod window;
pub mod workspace;

static CONTEXT: Mutex<OnceLock<VscodeContext>> = Mutex::new(OnceLock::new());

struct VscodeContext {
    vscode: bindgen_prelude::ObjectRef,
    extension_context: bindgen_prelude::ObjectRef,
}

impl VscodeContext {
    fn inner() -> Result<MappedMutexGuard<'static, Self>, napi::Error> {
        let mutex_guard = CONTEXT.lock().map_err(|_error| {
            napi::Error::from_reason("vscode_sys::Context mutex has been poisoned")
        })?;

        MutexGuard::filter_map(mutex_guard, |guarded_value| match guarded_value.get_mut() {
            Some(vscode_context) => Some(vscode_context),
            None => None,
        })
        .map_err(|_empty_guard| {
            napi::Error::new(napi::Status::GenericFailure, "`vscode` object is not set")
        })
    }

    fn extension_context<'env>(
        env: &'env napi::Env,
    ) -> Result<bindgen_prelude::Object<'env>, napi::Error> {
        let context = Self::inner()?;

        context.extension_context.get_value(env)
    }

    fn vscode<'env>(env: &'env napi::Env) -> Result<bindgen_prelude::Object<'env>, napi::Error> {
        let context = Self::inner()?;

        context.vscode.get_value(env)
    }
}

pub fn activate(
    vscode_object: &bindgen_prelude::Object,
    extension_context: &bindgen_prelude::Object,
) -> Result<(), napi::Error> {
    let mutex_guard = CONTEXT.lock().map_err(|_error| {
        napi::Error::from_reason(
            "vscode_sys::Context mutex was poisoned before it could be initialized",
        )
    })?;
    let context = VscodeContext {
        vscode: vscode_object.create_ref()?,
        extension_context: extension_context.create_ref()?,
    };

    mutex_guard.set(context).map_err(|_error| {
        napi::Error::from_reason(
            "`activate()` has already been called, but must be called exactly once",
        )
    })?;

    Ok(())
}

class_wrapper_empty!(EventEmitter);
field_getter! {
    EventEmitter {
        // TODO: this should be a typed function
        "event": get_event -> bindgen_prelude::Unknown<'env>;
    }
}
function_caller! {
    EventEmitter {
        "fire":
            fire(
                data: bindgen_prelude::Object,
            ) -> ();
    }
}
class_wrapper!(FileDecoration(badge: &str, tooltip: &str, color: &ThemeColor));
class_wrapper!(MarkdownString(value: &str, support_theme_icons: bool));
field_setter! {
    MarkdownString {
        "supportHtml": set_support_html(bool);
    }
}
class_wrapper!(Position(line: u32, character: u32));
field_getter! {
    Position {
        "line": get_line -> u32;
    }
}
class_wrapper!(Range(start: &Position<'env>, end: &Position<'env>));
class_wrapper!(Selection(anchor: &Position<'env>, active: &Position<'env>));
field_getter! {
    Selection {
        "start": get_start -> Position<'env>;
        "end": get_end -> Position<'env>;
    }
}
class_wrapper!(ThemeColor(id: &str));
// TODO: private constructor
class_wrapper!(Uri(scheme: &str, authority: &str, path: &str, query: &str, fragment: &str));
field_getter! {
    Uri {
        "fsPath": get_fs_path -> String;
        "scheme": get_scheme -> String;
    }
}
empty_function_caller! {
    Uri {
        "toString":
            to_string() -> String;
    }
}
function_caller! {
    Uri {
        "with":
            with(
                change: UriWithChange,
            ) -> Uri<'env>;
    }
}
static_class_method! {
    Uri {
        "file":
            file(
                path: &str,
            ) -> Uri<'env>;
    }
}

interface_builder! {
    DecorationOptions {
        ("range": Range, range: &Range),
    }

    ("hoverMessage", hover_message: &MarkdownString),
    ("renderOptions", render_options: DecorationInstanceRenderOptions),
}
interface_builder! {
    DecorationInstanceRenderOptions {}

    ("after", after: ThemableDecorationAttachmentRenderOptions),
}
interface_builder! {
    DecorationRenderOptions {}

    ("after", after: ThemableDecorationAttachmentRenderOptions),
}
interface_builder! {
    FileDecorationProvider {
        F: set_provide_file_decoration =
            provide_file_decoration(
                uri: Uri,
                cancellation_token: bindgen_prelude::Object
            ) -> FileDecoration<'function_context>;
    }
}
field_setter! {
    FileDecorationProvider {
        "onDidChangeFileDecorations": set_on_did_change_file_decorations(bindgen_prelude::Unknown);
    }
}
function_setter! {
    FileDecorationProvider {
        "provideFileDecoration" {
            set_provide_file_decoration(
                uri: Uri,
                cancellation_token: bindgen_prelude::Object
            ) -> FileDecoration<'function_context>;

            bindgen_prelude::ObjectRef = |returned_result: Option<FileDecoration>| returned_result.map(|object| object.inner.create_ref()).transpose();
        }
    }
}
interface_builder! {
    FileSystemWatcher {}
}
object_event_handler! {
    FileSystemWatcher {
        "onDidChange": on_did_change(Uri);
        "onDidCreate": on_did_create(Uri);
        "onDidDelete": on_did_delete(Uri);
    }
}
interface_builder! {
    QuickDiffProvider {}
}
function_setter! {
    QuickDiffProvider {
        "provideOriginalResource" {
            set_provide_original_resource(
                uri: Uri<'function_context>,
                cancellation_token: bindgen_prelude::Object<'function_context>
            ) -> Uri<'function_context>;

            bindgen_prelude::ObjectRef = |returned_result: Option<Uri>| returned_result.map(|object| object.inner.create_ref()).transpose();
        }
    }
}
interface_builder! {
    SourceControl {}
}
field_setter! {
    SourceControl {
        "quickDiffProvider": set_quick_diff_provider(QuickDiffProvider);
    }
}
function_caller! {
    SourceControl {
        "createResourceGroup":
            create_resource_group(
                id: &str,
                label: &str,
            ) -> SourceControlResourceGroup<'env>;
    }
}
interface_builder! {
    SourceControlResourceGroup {}
}
field_setter! {
    SourceControlResourceGroup {
        "resourceStates": set_resource_states(Vec<SourceControlResourceState>);
    }
}
interface_builder! {
    SourceControlResourceState {
        ("resourceUri": Uri, resource_uri: &Uri),
    }
}
interface_builder! {
    TextDocument {}
}
field_getter! {
    TextDocument {
        "isDirty": get_is_dirty -> bool;
        "uri": get_uri -> Uri<'env>;
    }
}
function_caller! {
    TextDocument {
        "getText":
            get_text(
                range: Option<Range>,
            ) -> String;
    }
}
interface_builder! {
    TextDocumentChangeEvent {}
}
field_getter! {
    TextDocumentChangeEvent {
        "document": get_document -> TextDocument<'env>;
        "contentChanges": get_content_changes -> Vec<TextDocumentContentChangeEvent<'env>>;
    }
}
interface_builder! {
    TextDocumentContentChangeEvent {}
}
field_getter! {
    TextDocumentContentChangeEvent {
        "rangeOffset": get_range_offset -> u32;
        "rangeLength": get_range_length -> u32;
        "text": get_text -> String;
    }
}
interface_builder! {
    TextDocumentContentProvider {
        F: set_provide_text_document_content =
            provide_text_document_content(
                uri: Uri<'function_context>,
                cancellation_token: bindgen_prelude::Object<'function_context>
            ) -> String;
    }
}
function_setter! {
    TextDocumentContentProvider {
        "provideTextDocumentContent" {
            set_provide_text_document_content(
                uri: Uri<'function_context>,
                cancellation_token: bindgen_prelude::Object<'function_context>
            ) -> String;

            String = |returned_result: Option<String>| Ok(returned_result);
        }
    }
}
interface_builder! {
    TextEditor {}
}
field_getter! {
    TextEditor {
        "document": get_document -> TextDocument<'env>;
        "selections": get_selections -> Vec<Selection<'env>>;
    }
}
function_caller! {
    TextEditor {
        "setDecorations":
            set_decorations(
                decoration_type: TextEditorDecorationType,
                options: Vec<DecorationOptions>,
            ) -> ();
    }
}
interface_builder! {
    TextEditorDecorationType {}
}
interface_builder! {
    TextEditorSelectionChangeEvent {}
}
field_getter! {
    TextEditorSelectionChangeEvent {
        "textEditor": get_text_editor -> TextEditor<'env>;
        "selections": get_selections -> Vec<Selection<'env>>;
    }
}
interface_builder! {
    ThemableDecorationAttachmentRenderOptions {}

    ("color", color: &ThemeColor),
    ("contentText", content_text: &str),
    ("margin", margin: &str),
}
interface_builder! {
    UriWithChange {}

    ("scheme", scheme: &str),
    ("authority", authority: &str),
    ("path", path: &str),
    ("query", query: &str),
    ("fragment", fragment: &str),
}
interface_builder! {
    WorkspaceFolder {}
}
field_getter! {
    WorkspaceFolder {
        "uri": get_uri -> Uri<'env>;
    }
}
interface_builder! {
    WorkspaceFoldersChangeEvent {}
}
field_getter! {
    WorkspaceFoldersChangeEvent {
        "added": get_added -> Vec<WorkspaceFolder<'env>>;
        "removed": get_removed -> Vec<WorkspaceFolder<'env>>;
    }
}