use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, ToTokens};
use syn::Ident;

use crate::attribute_info::*;
use crate::data_type::*;
use crate::transaction_field::*;

pub struct ApplyTransaction<'a> {
    pub variable_name: &'a Ident,
    pub data_type: &'a DataType,
    pub attribute_infos: &'a Vec<AttributeInfo>,

    pub tracker: &'a Option<Ident>,
}

impl<'a> From<&'a TransactionField> for ApplyTransaction<'a> {
    fn from(tf: &'a TransactionField) -> Self {
        Self {
            variable_name: &tf.variable_name,
            data_type: &tf.data_type,
            attribute_infos: &tf.attribute_infos,

            tracker: &tf.tracker,
        }
    }
}

fn from_str_quote(
    convert_type: &Option<&TokenStream>,
    custom_into: &Option<&Ident>,
    target_type: &TokenStream,
) -> TokenStream {
    match (convert_type, custom_into) {
        (Some(convert_type), Some(custom_into)) => {
            quote! {
                use ron::from_str;
                use serde::Deserialize;

                let p: Option<#convert_type> = from_str(value).ok();
                let f: Option<#target_type> = p.map(|p| self.#custom_into(p));
            }
        }
        (Some(convert_type), None) => {
            quote! {
                use ron::from_str;
                use serde::Deserialize;

                let p: Option<#convert_type> = from_str(value).ok();
                let f: Option<#target_type> = p.map(|p| p.into());
            }
        }
        (None, _) => {
            quote! {
                use ron::from_str;
                use serde::Deserialize;

                let f = from_str(value).ok();
            }
        }
    }
}

impl<'a> ToTokens for ApplyTransaction<'a> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let var = &self.variable_name;
        let variable_string = var.to_string();
        let variable_str = variable_string.as_str();
        let data_type = &self.data_type;
        let parsed_variable = Ident::new("value", Span::call_site());
        let time_variable = Ident::new("time", Span::call_site());
        let last_change = format_ident!("{}_last_change", var);
        let tracker = self.tracker.as_ref().expect("tracker not set");

        let convert_type = AttributeInfo::find_convert_type(&self.attribute_infos);
        let custom_into = AttributeInfo::find_custom_into(&self.attribute_infos);
        let exec_function = AttributeInfo::find_apply_exec_function(&self.attribute_infos);

        let index_value = quote! {
            let pos = #parsed_variable
                .find('!')
                .ok_or(anyhow::Error::msg(format!(
                    "Failed parsing in ApplyTransaction '!'. Found {}",
                    stringify!(#parsed_variable)
                )))?;

            let (index, value) = #parsed_variable.split_at(pos);
            let value = value.trim_start_matches('!');

            let index: usize = index.parse()?;
        };

        let (set_value_indexed, set_value) = match exec_function {
            Some(exec_function) => (
                quote! {
                    if let Some(f) = f {
                        let old_value = std::mem::replace(&mut self.#var[index], f);

                        self.#exec_function(old_value, index, multi_mut, scene)?;
                    }
                },
                quote! {
                    if let Some(f) = f {
                        let old_value = std::mem::replace(&mut self.#var, f);

                        self.#exec_function(old_value, multi_mut, scene)?;
                    }
                },
            ),
            None => (
                quote! {
                    if let Some(f) = f {
                        self.#var[index] = f;
                    }
                },
                quote! {
                    if let Some(f) = f {
                        self.#var = f;
                    }
                },
            ),
        };

        // check features
        AttributeInfo::set_features(&self.attribute_infos, tokens);

        proc_macro2::TokenStream::from(match data_type {
            DataType::Array(array) => {
                let from_str = from_str_quote(&convert_type, &custom_into, &array.inner_type);

                quote! {
                    #variable_str => {
                        #index_value

                        if #time_variable > self.#tracker.#last_change[index] {
                            self.#tracker.#last_change[index] = #time_variable;

                            #from_str
                            #set_value_indexed
                        }
                    }
                }
            }
            DataType::Vector(vector) => {
                let from_str = from_str_quote(&convert_type, &custom_into, &vector.inner_type);

                quote! {
                    #variable_str => {
                        #index_value

                        if self.#tracker.#last_change.len() as i32 - 1 < index as i32 {
                            let size_difference = index as i32 - (self.#tracker.#last_change.len() as i32 - 1);

                            for _ in 0..size_difference {
                                self.#tracker.#last_change.push(std::time::Duration::default());
                            }
                        }

                        if #time_variable > self.#tracker.#last_change[index] {
                            self.#tracker.#last_change[index] = #time_variable;

                            #from_str
                            #set_value_indexed
                        }
                    }
                }
            }
            DataType::Other(ts) => {
                let from_str = from_str_quote(&convert_type, &custom_into, &ts);

                quote! {
                    #variable_str => {
                        if #time_variable > self.#tracker.#last_change {
                            self.#tracker.#last_change = #time_variable;

                            let value = #parsed_variable;
                            #from_str
                            #set_value
                        }
                    }
                }
            }
        })
        .to_tokens(tokens);
    }
}