Documentation
use std::collections::HashMap;

use procmeta::prelude::*;
use quote::{format_ident, ToTokens};
use syn::DeriveInput;

use crate::structure::get_fn_name_suffix;

#[derive(MetaParser)]
pub enum ParsedTypeAttr {
    #[name("pull")]
    Pull { source: Type },

    #[name("push")]
    Push { target: Type },
}

#[derive(MetaParser)]
pub enum ParsedFieldAttr {
    #[name("pull")]
    Pull {
        source: Option<Type>,
        map: Option<Ident>,
        assign: Option<Expr>,
    },

    #[name("push")]
    Push {
        target: Option<Type>,
        map: Option<Ident>,
        assign: Option<Expr>,
    },
}

///
/// pub fn pull_xxx(&mut self, source: xxx) {
///     self.aa = source.aa;
/// }
pub struct PullItemContent {
    pub source: Type,

    pub fields: TokenStream,
}

impl PullItemContent {
    pub fn new(source: Type) -> Self {
        Self {
            source,
            fields: quote!(),
        }
    }

    pub fn add_field(&mut self, ident: &Option<Ident>, map: Option<Ident>, assign: Option<Expr>) {
        let mut value = quote!(source.#ident);
        if let Some(map) = map {
            value = quote!(source.#map);
        }
        if let Some(assign) = assign {
            value = quote!(#assign);
        }
        let last_fields_token = &self.fields;
        self.fields = quote! {
            #last_fields_token
            self.#ident = #value;
        }
    }
}

impl From<PullItemContent> for TokenStream {
    fn from(value: PullItemContent) -> Self {
        let source = value.source;
        let fn_name = format_ident!(
            "pull{}",
            get_fn_name_suffix(source.to_token_stream().to_string())
        );
        let fields = value.fields;
        quote! {
            pub fn #fn_name (&mut self, source: #source) {
                #fields
            }
        }
    }
}

///
/// pub fn push_xxx(self, target: &mut xxx) {
///     target.aa = self.aa;
/// }
///
pub struct PushItemContent {
    pub target: Type,

    pub fields: TokenStream,
}

impl PushItemContent {
    pub fn new(target: Type) -> Self {
        Self {
            target,
            fields: quote!(),
        }
    }

    pub fn add_field(&mut self, ident: &Option<Ident>, map: Option<Ident>, assign: Option<Expr>) {
        let mut ident_token = quote!(#ident);
        if let Some(map) = map {
            ident_token = quote!(#map);
        }
        let mut value = quote!(self.#ident);
        if let Some(assign) = assign {
            value = quote!(#assign);
        }
        let last_fields_token = &self.fields;
        self.fields = quote! {
            #last_fields_token
            target.#ident_token = #value;
        }
    }
}

impl From<PushItemContent> for TokenStream {
    fn from(value: PushItemContent) -> Self {
        let target = value.target;
        let fn_name = format_ident!(
            "push{}",
            get_fn_name_suffix(target.to_token_stream().to_string())
        );
        let fields = value.fields;
        quote! {
            pub fn #fn_name (self, target: &mut #target) {
                #fields
            }
        }
    }
}

pub fn expand(input: DeriveInput) -> Result<TokenStream> {
    let ty = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
    match input.data {
        syn::Data::Struct(data) => {
            let mut first_source_name = String::default();
            let mut pull_items: HashMap<String, PullItemContent> = HashMap::new();

            let mut first_target_name = String::default();
            let mut push_items: HashMap<String, PushItemContent> = HashMap::new();

            let mut result = quote!();
            for attr in input.attrs {
                let parsed_attr = <ParsedTypeAttr as MetaParser>::parse(&attr.meta)?;
                match parsed_attr {
                    ParsedTypeAttr::Pull { source } => {
                        let key = source.to_token_stream().to_string();
                        if first_source_name.is_empty() {
                            first_source_name = key.clone();
                        }
                        let content = PullItemContent::new(source);
                        pull_items.insert(key, content);
                    }
                    ParsedTypeAttr::Push { target } => {
                        let key = target.to_token_stream().to_string();
                        if first_target_name.is_empty() {
                            first_target_name = key.clone();
                        }
                        let content = PushItemContent::new(target);
                        push_items.insert(key, content);
                    }
                }
            }
            for field in data.fields {
                for attr in &field.attrs {
                    let parsed_attr = <ParsedFieldAttr as MetaParser>::parse(&attr.meta)?;
                    match parsed_attr {
                        ParsedFieldAttr::Pull {
                            source,
                            map,
                            assign,
                        } => {
                            let key = source
                                .map(|s| s.to_token_stream().to_string())
                                .unwrap_or(first_source_name.clone());
                            let pull_item_content = pull_items.get_mut(&key);
                            let pull_item_content = pull_item_content.ok_or(Error::new(
                                field.span(),
                                "expected already define source type",
                            ))?;
                            pull_item_content.add_field(&field.ident, map, assign);
                        }
                        ParsedFieldAttr::Push {
                            target,
                            map,
                            assign,
                        } => {
                            let key = target
                                .map(|s| s.to_token_stream().to_string())
                                .unwrap_or(first_target_name.clone());
                            let push_item_content = push_items.get_mut(&key);
                            let push_item_content = push_item_content.ok_or(Error::new(
                                field.span(),
                                "expected already define target type",
                            ))?;
                            push_item_content.add_field(&field.ident, map, assign);
                        }
                    }
                }
            }
            for v in pull_items.into_values() {
                let item_token: TokenStream = TokenStream::from(v);
                result = quote! {
                    #result
                    #item_token
                }
            }
            for v in push_items.into_values() {
                let item_token: TokenStream = TokenStream::from(v);
                result = quote! {
                    #result
                    #item_token
                }
            }
            result = quote! {
                impl #impl_generics #ty #ty_generics #where_clause  {
                    #result
                }
            };
            Ok(result)
        }
        _ => unimplemented!(),
    }
}