/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::Result;

use crate::util::mod_path;
use uniffi_meta::UNIFFI_CONTRACT_VERSION;

pub fn setup_scaffolding(namespace: String) -> Result<TokenStream> {
    let module_path = mod_path()?;
    let normalized_module_path = module_path.replace("::", "__");
    let ffi_contract_version_ident =
        format_ident!("ffi_{normalized_module_path}_uniffi_contract_version");
    let namespace_upper = namespace.to_ascii_uppercase();
    let namespace_const_ident = format_ident!("UNIFFI_META_CONST_NAMESPACE_{namespace_upper}");
    let namespace_static_ident = format_ident!("UNIFFI_META_NAMESPACE_{namespace_upper}");
    let ffi_rustbuffer_alloc_ident = format_ident!("ffi_{normalized_module_path}_rustbuffer_alloc");
    let ffi_rustbuffer_from_bytes_ident =
        format_ident!("ffi_{normalized_module_path}_rustbuffer_from_bytes");
    let ffi_rustbuffer_free_ident = format_ident!("ffi_{normalized_module_path}_rustbuffer_free");
    let ffi_rustbuffer_reserve_ident =
        format_ident!("ffi_{normalized_module_path}_rustbuffer_reserve");
    let reexport_hack_ident = format_ident!("{normalized_module_path}_uniffi_reexport_hack");
    let ffi_rust_future_scaffolding_fns = rust_future_scaffolding_fns(&normalized_module_path);

    Ok(quote! {
        // Unit struct to parameterize the FfiConverter trait.
        //
        // We use FfiConverter<UniFfiTag> to handle lowering/lifting/serializing types for this crate.  See
        // https://mozilla.github.io/uniffi-rs/internals/lifting_and_lowering.html#code-generation-and-the-fficonverter-trait
        // for details.
        //
        // This is pub, since we need to access it to support external types
        #[doc(hidden)]
        pub struct UniFfiTag;

        #[allow(clippy::missing_safety_doc, missing_docs)]
        #[doc(hidden)]
        #[unsafe(no_mangle)]
        pub extern "C" fn #ffi_contract_version_ident() -> ::std::primitive::u32 {
            #UNIFFI_CONTRACT_VERSION
        }


        /// Export namespace metadata.
        ///
        /// See `uniffi_bindgen::macro_metadata` for how this is used.

        const #namespace_const_ident: ::uniffi::MetadataBuffer =
            ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::NAMESPACE)
                .concat_str(#module_path)
                .concat_str(#namespace);

        #[doc(hidden)]
        #[unsafe(no_mangle)]
        pub static #namespace_static_ident: [::std::primitive::u8; #namespace_const_ident.size] =
            #namespace_const_ident.into_array();

        // Everybody gets basic buffer support, since it's needed for passing complex types over the FFI.
        //
        // See `uniffi/src/ffi/rustbuffer.rs` for documentation on these functions

        #[allow(clippy::missing_safety_doc, missing_docs)]
        #[doc(hidden)]
        #[unsafe(no_mangle)]
        pub extern "C" fn #ffi_rustbuffer_alloc_ident(
            size: ::std::primitive::u64,
            call_status: &mut ::uniffi::RustCallStatus,
        ) -> ::uniffi::RustBuffer {
            ::uniffi::ffi::uniffi_rustbuffer_alloc(size, call_status)
        }

        #[allow(clippy::missing_safety_doc, missing_docs)]
        #[doc(hidden)]
        #[unsafe(no_mangle)]
        pub unsafe extern "C" fn #ffi_rustbuffer_from_bytes_ident(
            bytes: ::uniffi::ForeignBytes,
            call_status: &mut ::uniffi::RustCallStatus,
        ) -> ::uniffi::RustBuffer {
            ::uniffi::ffi::uniffi_rustbuffer_from_bytes(bytes, call_status)
        }

        #[allow(clippy::missing_safety_doc, missing_docs)]
        #[doc(hidden)]
        #[unsafe(no_mangle)]
        pub unsafe extern "C" fn #ffi_rustbuffer_free_ident(
            buf: ::uniffi::RustBuffer,
            call_status: &mut ::uniffi::RustCallStatus,
        ) {
            ::uniffi::ffi::uniffi_rustbuffer_free(buf, call_status);
        }

        #[allow(clippy::missing_safety_doc, missing_docs)]
        #[doc(hidden)]
        #[unsafe(no_mangle)]
        pub unsafe extern "C" fn #ffi_rustbuffer_reserve_ident(
            buf: ::uniffi::RustBuffer,
            additional: ::std::primitive::u64,
            call_status: &mut ::uniffi::RustCallStatus,
        ) -> ::uniffi::RustBuffer {
            ::uniffi::ffi::uniffi_rustbuffer_reserve(buf, additional, call_status)
        }

        #ffi_rust_future_scaffolding_fns

        // Code to re-export the UniFFI scaffolding functions.
        //
        // Rust won't always re-export the functions from dependencies
        // ([rust-lang#50007](https://github.com/rust-lang/rust/issues/50007))
        //
        // A workaround for this is to have the dependent crate reference a function from its dependency in
        // an extern "C" function. This is clearly hacky and brittle, but at least we have some unittests
        // that check if this works (fixtures/reexport-scaffolding-macro).
        //
        // The main way we use this macro is for that contain multiple UniFFI components (libxul,
        // megazord).  The combined library has a cargo dependency for each component and calls
        // uniffi_reexport_scaffolding!() for each one.

        #[allow(missing_docs)]
        #[doc(hidden)]
        pub const fn uniffi_reexport_hack() {}

        #[doc(hidden)]
        #[macro_export]
        macro_rules! uniffi_reexport_scaffolding {
            () => {
                #[doc(hidden)]
                #[unsafe(no_mangle)]
                pub extern "C" fn #reexport_hack_ident() {
                    $crate::uniffi_reexport_hack()
                }
            };
        }
    })
}

/// Generates the rust_future_* functions
///
/// The foreign side uses a type-erased `Handle` to interact with futures, which presents
/// a problem when creating scaffolding functions.  What is the `ReturnType` parameter of `RustFutureFfi`?
///
/// Handle this by using some brute-force monomorphization.  For each possible ffi type, we
/// generate a set of scaffolding functions.  The bindings code is responsible for calling the one
/// corresponds the scaffolding function that created the `Handle`.
///
/// This introduces safety issues, but we do get some type checking.  If the bindings code calls
/// the wrong rust_future_complete function, they should get an unexpected return type, which
/// hopefully will result in a compile-time error.
fn rust_future_scaffolding_fns(module_path: &str) -> TokenStream {
    let fn_info = [
        (quote! { u8 }, "u8"),
        (quote! { i8 }, "i8"),
        (quote! { u16 }, "u16"),
        (quote! { i16 }, "i16"),
        (quote! { u32 }, "u32"),
        (quote! { i32 }, "i32"),
        (quote! { u64 }, "u64"),
        (quote! { i64 }, "i64"),
        (quote! { f32 }, "f32"),
        (quote! { f64 }, "f64"),
        (quote! { *const ::std::ffi::c_void }, "pointer"),
        (quote! { ::uniffi::RustBuffer }, "rust_buffer"),
        (quote! { () }, "void"),
    ];
    fn_info.iter()
    .map(|(return_type, fn_suffix)| {
        let ffi_rust_future_poll = format_ident!("ffi_{module_path}_rust_future_poll_{fn_suffix}");
        let ffi_rust_future_cancel = format_ident!("ffi_{module_path}_rust_future_cancel_{fn_suffix}");
        let ffi_rust_future_complete = format_ident!("ffi_{module_path}_rust_future_complete_{fn_suffix}");
        let ffi_rust_future_free = format_ident!("ffi_{module_path}_rust_future_free_{fn_suffix}");

        quote! {
            #[allow(clippy::missing_safety_doc, missing_docs)]
            #[doc(hidden)]
            #[unsafe(no_mangle)]
            pub unsafe extern "C" fn #ffi_rust_future_poll(handle: ::uniffi::Handle, callback: ::uniffi::RustFutureContinuationCallback, data: u64) {
                ::uniffi::ffi::rust_future_poll::<#return_type, crate::UniFfiTag>(handle, callback, data);
            }

            #[allow(clippy::missing_safety_doc, missing_docs)]
            #[doc(hidden)]
            #[unsafe(no_mangle)]
            pub unsafe extern "C" fn #ffi_rust_future_cancel(handle: ::uniffi::Handle) {
                ::uniffi::ffi::rust_future_cancel::<#return_type, crate::UniFfiTag>(handle)
            }

            #[allow(clippy::missing_safety_doc, missing_docs)]
            #[doc(hidden)]
            #[unsafe(no_mangle)]
            pub unsafe extern "C" fn #ffi_rust_future_complete(
                handle: ::uniffi::Handle,
                out_status: &mut ::uniffi::RustCallStatus
            ) -> #return_type {
                ::uniffi::ffi::rust_future_complete::<#return_type, crate::UniFfiTag>(handle, out_status)
            }

            #[allow(clippy::missing_safety_doc, missing_docs)]
            #[doc(hidden)]
            #[unsafe(no_mangle)]
            pub unsafe extern "C" fn #ffi_rust_future_free(handle: ::uniffi::Handle) {
                ::uniffi::ffi::rust_future_free::<#return_type, crate::UniFfiTag>(handle)
            }
        }
    })
    .collect()
}
