//! # derive_into_owned
//!
//! This crate supports deriving two different methods (not traits):
//!
//!  * `IntoOwned`
//!  * `Borrowed`
//!
//! These were first created to help out with types generated by [`quick_protobuf`] which generates
//! structs with [`Cow`] fields. It is entirely possible that this crate is not needed at all and
//! that there exists better alternatives to what this crate aims to support.
//!
//! ## Definitions, naming
//!
//! "Cow-alike" is used to mean a type that:
//!
//!  * has a lifetime specifier, like `Foo<'a>`
//!  * has an implementation for `fn into_owned(self) -> Foo<'static>`
//!
//! This is a bit of a bad name as [`Cow`] itself has a different kind of `into_owned` which
//! returns the owned version of the generic type parameter. I am open to suggestions for both
//! "Cow-alike" and `into_owned`.
//!
//! ## `IntoOwned`
//!
//! `#[derive(IntoOwned)]` implements a method `fn into_owned(self) -> Foo<'static>` for type
//! `Foo<'a>` which contains [`Cow`] or "Cow-alike" fields. The method returns a version with
//! `'static` lifetime which means the value owns all of it's data. This is useful if you are
//! for example, working with [`tokio-rs`] which currently requires types to be `'static`.
//!
//! ## `Borrowed`
//!
//! `#[derive(Borrowed)]` implements a method `fn borrowed<'b>(&'b self) -> Foo<'b>` for type
//! `Foo<'a>`. This is useful in case you need to transform the value into another type using
//! std conversions like [`From`], but you don't want to clone the data in the process. Note that
//! the all the fields that are not [`Cow`] or "Cow-alike" are just cloned, and new vectors are
//! collected, so this yields savings only when you manage to save big chunks of memory.
//!
//! ## Limitations
//!
//! Currently only the types I needed are supported and this might be a rather limited set of
//! functionality. If you find that this does not work in your case please file an issue at [project
//! repository](https://github.com/koivunej/derive-into-owned/issues).
//!
//! [`quick_protobuf`]: https://github.com/tafia/quick-protobuf/
//! [`tokio-rs`]: https://tokio.rs
//! [`Cow`]: https://doc.rust-lang.org/std/borrow/enum.Cow.html
//! [`From`]: https://doc.rust-lang.org/std/convert/trait.From.html

use quote::{format_ident, quote};

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

mod field_kind;
mod helpers;

use field_kind::FieldKind;

#[proc_macro_derive(IntoOwned)]
pub fn into_owned(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);

    let expanded = impl_with_generator(&ast, IntoOwnedGen);

    TokenStream::from(expanded)
}

#[proc_macro_derive(Borrowed)]
pub fn borrowed(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);

    let expanded = impl_with_generator(&ast, BorrowedGen);

    TokenStream::from(expanded)
}

fn impl_with_generator<G: BodyGenerator>(
    ast: &syn::DeriveInput,
    gen: G,
) -> proc_macro2::TokenStream {
    // this is based heavily on https://github.com/asajeffrey/deep-clone/blob/master/deep-clone-derive/lib.rs
    let name = &ast.ident;

    let borrowed_params = gen.quote_borrowed_params(ast);
    let borrowed = if borrowed_params.is_empty() {
        quote! {}
    } else {
        quote! { < #(#borrowed_params),* > }
    };

    let params = gen.quote_type_params(ast);
    let params = if params.is_empty() {
        quote! {}
    } else {
        quote! { < #(#params),* > }
    };

    let owned_params = gen.quote_rhs_params(ast);
    let owned = if owned_params.is_empty() {
        quote! {}
    } else {
        quote! { < #(#owned_params),* > }
    };

    let body = match ast.data {
        syn::Data::Struct(ref body) => {
            let inner = gen.visit_struct(body);
            quote! { #name #inner }
        }
        syn::Data::Enum(ref body) => {
            let cases = body.variants.iter().map(|variant| {
                let unqualified_ident = &variant.ident;
                let ident = quote! { #name::#unqualified_ident };

                gen.visit_enum_data(ident, variant)
            });
            quote! { match self { #(#cases),* } }
        }
        syn::Data::Union(_) => todo!("unions are not supported (5)"),
    };

    gen.combine_impl(borrowed, name, params, owned, body)
}

/// Probably not the best abstraction
trait BodyGenerator {
    fn quote_borrowed_params(&self, ast: &syn::DeriveInput) -> Vec<proc_macro2::TokenStream> {
        let borrowed_lifetime_params = ast.generics.lifetimes().map(|alpha| quote! { #alpha });
        let borrowed_type_params = ast.generics.type_params().map(|ty| quote! { #ty });
        borrowed_lifetime_params
            .chain(borrowed_type_params)
            .collect::<Vec<_>>()
    }

    fn quote_type_params(&self, ast: &syn::DeriveInput) -> Vec<proc_macro2::TokenStream> {
        ast.generics
            .lifetimes()
            .map(|alpha| quote! { #alpha })
            .chain(ast.generics.type_params().map(|ty| {
                let ident = &ty.ident;
                quote! { #ident }
            }))
            .collect::<Vec<_>>()
    }

    fn quote_rhs_params(&self, ast: &syn::DeriveInput) -> Vec<proc_macro2::TokenStream> {
        let owned_lifetime_params = ast.generics.lifetimes().map(|_| quote! { 'static });
        let owned_type_params = ast.generics.type_params().map(|ty| {
            let ident = &ty.ident;
            quote! { #ident }
        });
        owned_lifetime_params
            .chain(owned_type_params)
            .collect::<Vec<_>>()
    }

    fn visit_struct(&self, data: &syn::DataStruct) -> proc_macro2::TokenStream;
    fn visit_enum_data(
        &self,
        variant: proc_macro2::TokenStream,
        data: &syn::Variant,
    ) -> proc_macro2::TokenStream;
    fn combine_impl(
        &self,
        borrows: proc_macro2::TokenStream,
        name: &syn::Ident,
        rhs_params: proc_macro2::TokenStream,
        owned: proc_macro2::TokenStream,
        body: proc_macro2::TokenStream,
    ) -> proc_macro2::TokenStream;
}

struct IntoOwnedGen;

impl BodyGenerator for IntoOwnedGen {
    fn visit_struct(&self, data: &syn::DataStruct) -> proc_macro2::TokenStream {
        // Helper ternary to avoid Option<bool>
        enum Fields {
            Named,
            Tuple,
            Unit,
        }

        use Fields::*;

        let fields_kind = data
            .fields
            .iter()
            .next()
            .map(|field| if field.ident.is_some() { Named } else { Tuple })
            .unwrap_or(Unit);

        match fields_kind {
            Named => {
                let fields = data.fields.iter().map(|field| {
                    let ident = field.ident.as_ref().expect("unexpected unnamed field");
                    let field_ref = quote! { self.#ident };
                    let code = FieldKind::resolve(&field.ty).move_or_clone_field(&field_ref);
                    quote! { #ident: #code }
                });
                quote! { { #(#fields),* } }
            }
            Tuple => {
                let fields = data.fields.iter().enumerate().map(|(index, field)| {
                    let index = syn::Index::from(index);
                    let index = quote! { self.#index };
                    FieldKind::resolve(&field.ty).move_or_clone_field(&index)
                });
                quote! { ( #(#fields),* ) }
            }
            Unit => {
                quote! {}
            }
        }
    }

    fn visit_enum_data(
        &self,
        ident: proc_macro2::TokenStream,
        variant: &syn::Variant,
    ) -> proc_macro2::TokenStream {
        if variant.fields.is_empty() {
            return quote!(#ident => #ident);
        }

        let fields_are_named = variant.fields.iter().any(|field| field.ident.is_some());

        if fields_are_named {
            let named_fields = variant
                .fields
                .iter()
                .filter_map(|field| field.ident.as_ref());

            let cloned = variant.fields.iter().map(|field| {
                let ident = field.ident.as_ref().unwrap();
                let ident = quote!(#ident);
                let code = FieldKind::resolve(&field.ty).move_or_clone_field(&ident);
                quote! { #ident: #code }
            });
            quote! { #ident { #(#named_fields),* } => #ident { #(#cloned),* } }
        } else {
            let unnamed_fields = &variant
                .fields
                .iter()
                .enumerate()
                .filter(|(_, field)| field.ident.is_none())
                .map(|(index, _)| format_ident!("arg_{}", index))
                .collect::<Vec<_>>();

            let cloned = unnamed_fields
                .iter()
                .zip(variant.fields.iter())
                .map(|(ident, field)| {
                    let ident = quote! { #ident };
                    FieldKind::resolve(&field.ty).move_or_clone_field(&ident)
                })
                .collect::<Vec<_>>();

            quote! { #ident ( #(#unnamed_fields),* ) => #ident ( #(#cloned),* ) }
        }
    }

    fn combine_impl(
        &self,
        borrowed: proc_macro2::TokenStream,
        name: &syn::Ident,
        params: proc_macro2::TokenStream,
        owned: proc_macro2::TokenStream,
        body: proc_macro2::TokenStream,
    ) -> proc_macro2::TokenStream {
        quote! {
            impl #borrowed #name #params {
                /// Returns a version of `self` with all fields converted to owning versions.
                pub fn into_owned(self) -> #name #owned { #body }
            }
        }
    }
}

struct BorrowedGen;

impl BodyGenerator for BorrowedGen {
    fn quote_rhs_params(&self, ast: &syn::DeriveInput) -> Vec<proc_macro2::TokenStream> {
        let owned_lifetime_params = ast.generics.lifetimes().map(|_| quote! { '__borrowedgen });
        let owned_type_params = ast.generics.type_params().map(|ty| {
            let ident = &ty.ident;
            quote! { #ident }
        });
        owned_lifetime_params
            .chain(owned_type_params)
            .collect::<Vec<_>>()
    }

    fn visit_struct(&self, data: &syn::DataStruct) -> proc_macro2::TokenStream {
        let fields = data.fields.iter().map(|field| {
            let ident = field.ident.as_ref().expect("this fields has no ident (4)");
            let field_ref = quote! { self.#ident };
            let code = FieldKind::resolve(&field.ty).borrow_or_clone(&field_ref);
            quote! { #ident: #code }
        });
        quote! { { #(#fields),* } }
    }

    fn visit_enum_data(
        &self,
        ident: proc_macro2::TokenStream,
        variant: &syn::Variant,
    ) -> proc_macro2::TokenStream {
        if variant.fields.is_empty() {
            return quote!(#ident => #ident);
        }

        let fields_are_named = variant.fields.iter().any(|field| field.ident.is_some());
        if fields_are_named {
            let idents = variant
                .fields
                .iter()
                .map(|field| field.ident.as_ref().expect("this fields has no ident (5)"));
            let cloned = variant.fields.iter().map(|field| {
                let ident = field.ident.as_ref().expect("this fields has no ident (6)");
                let ident = quote! { #ident };
                let code = FieldKind::resolve(&field.ty).borrow_or_clone(&ident);
                quote! { #ident: #code }
            });
            quote! { #ident { #(ref #idents),* } => #ident { #(#cloned),* } }
        } else {
            let idents = (0..variant.fields.len())
                .map(|index| quote::format_ident!("x{}", index))
                .collect::<Vec<_>>();
            let cloned = idents
                .iter()
                .zip(variant.fields.iter())
                .map(|(ident, field)| {
                    let ident = quote! { #ident };
                    FieldKind::resolve(&field.ty).borrow_or_clone(&ident)
                })
                .collect::<Vec<_>>();
            quote! { #ident ( #(ref #idents),* ) => #ident ( #(#cloned),* ) }
        }
    }

    fn combine_impl(
        &self,
        borrowed: proc_macro2::TokenStream,
        name: &syn::Ident,
        params: proc_macro2::TokenStream,
        owned: proc_macro2::TokenStream,
        body: proc_macro2::TokenStream,
    ) -> proc_macro2::TokenStream {
        quote! {
            impl #borrowed #name #params {
                /// Returns a clone of `self` that shares all the "Cow-alike" data with `self`.
                pub fn borrowed<'__borrowedgen>(&'__borrowedgen self) -> #name #owned { #body }
            }
        }
    }
}
