/* 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/. */

//! General handling for the derive and udl_derive macros

use crate::util::kw;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{
    parse::{Parse, ParseStream},
    DeriveInput,
};

pub fn expand_derive(
    kind: DeriveKind,
    input: DeriveInput,
    options: DeriveOptions,
) -> syn::Result<TokenStream> {
    match kind {
        DeriveKind::Record(_) => crate::record::expand_record(input, options),
        DeriveKind::Object(_) => crate::object::expand_object(input, options),
        DeriveKind::Enum(_) => crate::enum_::expand_enum(input, options),
        DeriveKind::Error(_) => crate::error::expand_error(input, options),
    }
}

pub enum DeriveKind {
    Record(kw::Record),
    Enum(kw::Enum),
    Error(kw::Error),
    Object(kw::Object),
}

impl Parse for DeriveKind {
    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(kw::Record) {
            Ok(Self::Record(input.parse()?))
        } else if lookahead.peek(kw::Enum) {
            Ok(Self::Enum(input.parse()?))
        } else if lookahead.peek(kw::Error) {
            Ok(Self::Error(input.parse()?))
        } else if lookahead.peek(kw::Object) {
            Ok(Self::Object(input.parse()?))
        } else {
            Err(lookahead.error())
        }
    }
}

pub struct DeriveOptions {
    /// Should we implement FFI traits for the local UniFfiTag only?
    pub local_tag: bool,
    /// Should we generate metadata symbols?
    pub generate_metadata: bool,
}

/// default() is used to construct a DeriveOptions for a regular `derive` invocation
impl Default for DeriveOptions {
    fn default() -> Self {
        Self {
            local_tag: false,
            generate_metadata: true,
        }
    }
}

impl DeriveOptions {
    /// Construct DeriveOptions for `udl_derive`
    pub fn udl_derive() -> Self {
        Self {
            local_tag: true,
            generate_metadata: false,
        }
    }

    /// Generate the impl header for a FFI trait
    ///
    /// This will output something like `impl<UT> FfiConverter<UT> for #type`.  The caller is
    /// responsible for providing the body if the impl block.
    pub fn ffi_impl_header(&self, trait_name: &str, ident: &impl ToTokens) -> TokenStream {
        let trait_name = Ident::new(trait_name, Span::call_site());
        if self.local_tag {
            quote! { impl ::uniffi::#trait_name<crate::UniFfiTag> for #ident }
        } else {
            quote! { impl<UT> ::uniffi::#trait_name<UT> for #ident }
        }
    }

    /// Generate a call to `derive_ffi_traits!` that will derive all the FFI traits
    pub fn derive_all_ffi_traits(&self, ty: &Ident) -> TokenStream {
        if self.local_tag {
            quote! { ::uniffi::derive_ffi_traits!(local #ty); }
        } else {
            quote! { ::uniffi::derive_ffi_traits!(blanket #ty); }
        }
    }

    /// Generate a call to `derive_ffi_traits!` that will derive some of the FFI traits
    pub fn derive_ffi_traits(&self, ty: impl ToTokens, trait_names: &[&str]) -> TokenStream {
        let trait_idents = trait_names
            .iter()
            .map(|name| Ident::new(name, Span::call_site()));
        if self.local_tag {
            quote! {
                #(
                    ::uniffi::derive_ffi_traits!(impl #trait_idents<crate::UniFfiTag> for #ty);
                )*
            }
        } else {
            quote! {
                #(
                    ::uniffi::derive_ffi_traits!(impl<UT> #trait_idents<UT> for #ty);
                )*
            }
        }
    }
}
