extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{Item as SynItem, ItemFn};

#[proc_macro_attribute]
pub fn plugin_transform(
    _args: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let token = proc_macro2::TokenStream::from(input);
    let parsed_results = syn::parse2::<SynItem>(token).expect("Failed to parse tokens");
    match parsed_results {
        SynItem::Fn(func) => handle_func(func, Ident::new("Program", Span::call_site())),
        _ => panic!("Please confirm if plugin macro is specified for the function"),
    }
}

#[proc_macro_attribute]
pub fn css_plugin_transform(
    _args: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let token = proc_macro2::TokenStream::from(input);
    let parsed_results = syn::parse2::<SynItem>(token).expect("Failed to parse tokens");
    match parsed_results {
        SynItem::Fn(func) => handle_func(func, Ident::new("Stylesheet", Span::call_site())),
        _ => panic!("Please confirm if plugin macro is specified for the function"),
    }
}

#[allow(clippy::redundant_clone)]
fn handle_func(func: ItemFn, ast_type: Ident) -> TokenStream {
    let ident = func.sig.ident.clone();
    let transform_process_impl_ident =
        Ident::new("__transform_plugin_process_impl", Span::call_site());
    let transform_core_pkg_diag_ident =
        Ident::new("__get_transform_plugin_core_pkg_diag", Span::call_site());

    let ret = quote! {
        #func

        // Declaration for imported function from swc host.
        // Refer swc_plugin_runner for the actual implementation.
        #[cfg(target_arch = "wasm32")] // Allow testing
        extern "C" {
            fn __set_transform_result(bytes_ptr: u32, bytes_ptr_len: u32);
            fn __set_transform_plugin_core_pkg_diagnostics(bytes_ptr: u32, bytes_ptr_len: u32);
            fn __emit_diagnostics(bytes_ptr: u32, bytes_ptr_len: u32);
        }

        /// An emitter for the Diagnostic in plugin's context by borrowing host's
        /// environment context.
        ///
        /// It is not expected to call this directly inside of plugin transform.
        /// Instead, it is encouraged to use global HANDLER.
        pub struct PluginDiagnosticsEmitter;

        impl swc_core::common::errors::Emitter for PluginDiagnosticsEmitter {
            #[cfg_attr(not(target_arch = "wasm32"), allow(unused))]
            fn emit(&mut self, db: &mut swc_core::common::errors::DiagnosticBuilder<'_>) {
                let diag = swc_core::common::plugin::serialized::PluginSerializedBytes::try_serialize(&swc_core::common::plugin::serialized::VersionedSerializable::new(*db.diagnostic.clone()))
                    .expect("Should able to serialize Diagnostic");
                let (ptr, len) = diag.as_ptr();

                #[cfg(target_arch = "wasm32")] // Allow testing
                unsafe {
                    __emit_diagnostics(ptr as u32, len as u32);
                }
            }
        }


        /// Call hosts's imported fn to set transform results.
        /// __set_transform_result is host side imported fn, which read and copies guest's byte into host.
        fn send_transform_result_to_host(bytes_ptr: u32, bytes_ptr_len: u32) {
            #[cfg(target_arch = "wasm32")] // Allow testing
            unsafe {
                __set_transform_result(bytes_ptr, bytes_ptr_len);
            }
        }

        /// Internal function plugin_macro uses to create ptr to PluginError.
        fn construct_error_ptr(plugin_error: swc_core::common::plugin::serialized::PluginError) -> u32 {
            let ret = swc_core::common::plugin::serialized::PluginSerializedBytes::try_serialize(&swc_core::common::plugin::serialized::VersionedSerializable::new(plugin_error)).expect("Should able to serialize PluginError");
            let (ptr, len) = ret.as_ptr();

            send_transform_result_to_host(
                ptr as _,
                len as u32
            );
            1
        }

        #[no_mangle]
        #[allow(clippy::not_unsafe_ptr_arg_deref)]
        pub fn #transform_core_pkg_diag_ident() -> u32 {
            let schema_version = swc_core::common::plugin::PLUGIN_TRANSFORM_AST_SCHEMA_VERSION;
            let core_pkg_diag = swc_core::diagnostics::get_core_engine_diagnostics();

            let result = swc_core::common::plugin::diagnostics::PluginCorePkgDiagnostics {
                ast_schema_version: schema_version,
                pkg_version: core_pkg_diag.package_semver,
                git_sha: core_pkg_diag.git_sha,
                cargo_features: core_pkg_diag.cargo_features,
            };

            let serialized_result = swc_core::common::plugin::serialized::PluginSerializedBytes::try_serialize(
                &swc_core::common::plugin::serialized::VersionedSerializable::new(result)
            ).expect("Diagnostics should be always serializable");

            let (serialized_result_ptr, serialized_result_ptr_len) = serialized_result.as_ptr();

            #[cfg(target_arch = "wasm32")] // Allow testing
            unsafe {
                __set_transform_plugin_core_pkg_diagnostics(serialized_result_ptr as _, serialized_result_ptr_len as u32);
            }
            0
        }

        // Macro to allow compose plugin's transform function without manual pointer operation.
        // Internally it wraps pointer operation also bubbles up error in forms of PluginError.
        // There are some cases error won't be wrapped up however - for example, we expect
        // serialization of PluginError itself should succeed.
        #[no_mangle]
        #[allow(clippy::not_unsafe_ptr_arg_deref)]
        pub fn #transform_process_impl_ident(
            ast_ptr: *const u8, ast_ptr_len: u32,
            unresolved_mark: u32, should_enable_comments_proxy: i32) -> u32 {
            // Reconstruct `Program` & config string from serialized program
            // Host (SWC) should allocate memory, copy bytes and pass ptr to plugin.
            let program = swc_core::common::plugin::serialized::PluginSerializedBytes::from_raw_ptr(ast_ptr, ast_ptr_len.try_into().expect("Should able to convert ptr length")).deserialize();
            if program.is_err() {
                let err = swc_core::common::plugin::serialized::PluginError::Deserialize("Failed to deserialize program received from host".to_string());
                return construct_error_ptr(err);
            }
            let program: #ast_type = program.expect("Should be a program").into_inner();

            // Create a handler wired with plugin's diagnostic emitter, set it for global context.
            let handler = swc_core::common::errors::Handler::with_emitter(
                true,
                false,
                Box::new(PluginDiagnosticsEmitter)
            );

            // Construct metadata to the `Program` for the transform plugin.
            let plugin_comments_proxy = if should_enable_comments_proxy == 1 { Some(swc_core::plugin::proxies::PluginCommentsProxy) } else { None };
            let mut metadata = swc_core::plugin::metadata::TransformPluginProgramMetadata {
                comments: plugin_comments_proxy,
                source_map: swc_core::plugin::proxies::PluginSourceMapProxy { source_file: swc_core::common::sync::OnceCell::new() },
                unresolved_mark: swc_core::common::Mark::from_u32(unresolved_mark as u32),
            };

            // Take original plugin fn ident, then call it with interop'ed args
            let transformed_program = swc_core::common::plugin::serialized::VersionedSerializable::new(swc_core::common::errors::HANDLER.set(&handler, || {
                #ident(program, metadata)
            }));

            // Serialize transformed result, return back to the host.
            let serialized_result = swc_core::common::plugin::serialized::PluginSerializedBytes::try_serialize(
                &transformed_program
            );

            if serialized_result.is_err() {
                let err = swc_core::common::plugin::serialized::PluginError::Serialize("Failed to serialize transformed program".to_string());
                return construct_error_ptr(err);
            }

            let serialized_result = serialized_result.expect("Should be a realized transformed program");
            let (serialized_result_ptr, serialized_result_ptr_len) = serialized_result.as_ptr();

            send_transform_result_to_host(serialized_result_ptr as _, serialized_result_ptr_len as u32);
            0
        }
    };

    ret.into()
}
