use proc_macro2::TokenStream;
use syn::DeriveInput;

use field::Field;
use model::Model;
use util::wrap_in_dummy_mod;

pub fn derive(item: DeriveInput) -> TokenStream {
    let model = Model::from_item(&item, false);

    let (_, ty_generics, _) = item.generics.split_for_impl();

    let mut generics = item.generics.clone();
    generics
        .params
        .push(parse_quote!(__DB: diesel::backend::Backend));

    for embed_field in model.fields().iter().filter(|f| f.embed()) {
        let embed_ty = &embed_field.ty;
        generics
            .where_clause
            .get_or_insert_with(|| parse_quote!(where))
            .predicates
            .push(parse_quote!(#embed_ty: Selectable<__DB>));
    }

    let (impl_generics, _, where_clause) = generics.split_for_impl();

    let struct_name = &item.ident;

    let field_columns_ty = model.fields().iter().map(|f| field_column_ty(f, &model));
    let field_columns_inst = model.fields().iter().map(|f| field_column_inst(f, &model));

    wrap_in_dummy_mod(quote! {
        use diesel::expression::Selectable;

        impl #impl_generics Selectable<__DB>
            for #struct_name #ty_generics
        #where_clause
        {
            type SelectExpression = (#(#field_columns_ty,)*);

            fn construct_selection() -> Self::SelectExpression {
                (#(#field_columns_inst,)*)
            }
        }
    })
}

fn field_column_ty(field: &Field, model: &Model) -> TokenStream {
    if let Some(ref select_expression_type) = field.select_expression_type {
        let ty = &select_expression_type.item;
        quote!(#ty)
    } else if field.embed() {
        let embed_ty = &field.ty;
        quote!(<#embed_ty as Selectable<__DB>>::SelectExpression)
    } else {
        let table_name = model.table_name();
        let column_name = field.column_name();
        quote!(#table_name::#column_name)
    }
}

fn field_column_inst(field: &Field, model: &Model) -> TokenStream {
    use syn::spanned::Spanned;

    if let Some(ref select_expression) = field.select_expression {
        let expr = &select_expression.item;
        let span = expr.span();
        quote::quote_spanned!(span => #expr)
    } else if field.embed() {
        let embed_ty = &field.ty;
        quote!(<#embed_ty as Selectable<__DB>>::construct_selection())
    } else {
        let table_name = model.table_name();
        let column_name = field.column_name();
        quote!(#table_name::#column_name)
    }
}
