use proc_macro2 as m4;
use quote::quote;

use super::super::utilities::{entity_name, entity_union_name, field_name, func_name, usize_lit};
use crate::ast::{self as ast, HasName};

pub(in super::super) trait ImplSetters {
    fn impl_setters(&self) -> m4::TokenStream;
}

impl ImplSetters for ast::Option_ {
    fn impl_setters(&self) -> m4::TokenStream {
        let inner = entity_name(self.item().typ().name());
        quote!(
            pub fn set<T>(mut self, v: T) -> Self
            where
                T: ::core::convert::Into<Option<#inner>>
            {
                self.0 = v.into();
                self
            }
        )
    }
}

impl ImplSetters for ast::Union {
    fn impl_setters(&self) -> m4::TokenStream {
        let entity_union = entity_union_name(self.name());
        quote!(
            pub fn set<I>(mut self, v: I) -> Self
            where
                I: ::core::convert::Into<#entity_union>
            {
                self.0 = v.into();
                self
            }
        )
    }
}

impl ImplSetters for ast::Array {
    fn impl_setters(&self) -> m4::TokenStream {
        let inner = entity_name(self.item().typ().name());
        let item_count = usize_lit(self.item_count());
        let entire_setter = quote!(
            pub fn set<T>(mut self, v: T) -> Self
            where
                T: ::core::convert::Into<[#inner; #item_count]>
            {
                self.0 = v.into();
                self
            }
        );
        let each_setter = (0..self.item_count())
            .map(|idx| {
                let index = usize_lit(idx);
                let func = func_name(&format!("nth{}", idx));
                quote!(
                    pub fn #func<T>(mut self, v: T) -> Self
                    where
                        T: ::core::convert::Into<#inner>
                    {
                        self.0[#index] = v.into();
                        self
                    }
                )
            })
            .collect::<Vec<_>>();
        quote!(
            #entire_setter
            #( #each_setter )*
        )
    }
}

impl ImplSetters for ast::Struct {
    fn impl_setters(&self) -> m4::TokenStream {
        impl_setters_for_struct_or_table(self.fields())
    }
}

impl ImplSetters for ast::FixVec {
    fn impl_setters(&self) -> m4::TokenStream {
        impl_setters_for_vector(self.item().typ().name())
    }
}

impl ImplSetters for ast::DynVec {
    fn impl_setters(&self) -> m4::TokenStream {
        impl_setters_for_vector(self.item().typ().name())
    }
}

impl ImplSetters for ast::Table {
    fn impl_setters(&self) -> m4::TokenStream {
        impl_setters_for_struct_or_table(self.fields())
    }
}

fn impl_setters_for_struct_or_table(inner: &[ast::FieldDecl]) -> m4::TokenStream {
    let each_setter = inner
        .iter()
        .map(|f| {
            let field_name = field_name(f.name());
            let field_type = entity_name(f.typ().name());
            quote!(
                pub fn #field_name<T>(mut self, v: T) -> Self
                where
                    T: ::core::convert::Into<#field_type>
                {
                    self.#field_name = v.into();
                    self
                }
            )
        })
        .collect::<Vec<_>>();
    quote!(
        #( #each_setter )*
    )
}

fn impl_setters_for_vector(inner_name: &str) -> m4::TokenStream {
    let inner = entity_name(inner_name);
    quote!(
        pub fn set(mut self, v: Vec<#inner>) -> Self
        {
            self.0 = v;
            self
        }
        pub fn push<T>(mut self, v: T) -> Self
        where
            T: ::core::convert::Into<#inner>
        {
            self.0.push(v.into());
            self
        }
        pub fn extend<T: ::core::iter::IntoIterator<Item=#inner>>(mut self, iter: T) -> Self {
            self.0.extend(iter);
            self
        }
        pub fn replace<T>(&mut self, index: usize, v: T) -> Option<#inner>
        where
            T: ::core::convert::Into<#inner>
        {
            self.0.get_mut(index).map(|item| ::core::mem::replace(item, v.into()))
        }
    )
}
