use proc_macro2::{self, Span};
use quote::quote;
use syn;
use validator::Validator;
use crate::asserts::{COW_TYPE, NUMBER_TYPES};
use crate::lit::{option_f64_to_tokens, option_u64_to_tokens};
use crate::validation::{FieldValidation, SchemaValidation};
/// Pass around all the information needed for creating a validation
#[derive(Debug)]
pub struct FieldQuoter {
    ident: syn::Ident,
    /// The field name
    name: String,
    /// The field type
    _type: String,
}
impl FieldQuoter {
    pub fn new(ident: syn::Ident, name: String, _type: String) -> FieldQuoter {
        FieldQuoter { ident, name, _type }
    }
    /// Don't put a & in front a pointer since we are going to pass
    /// a reference to the validator
    /// Also just use the ident without if it's optional and will go through
    /// a if let first
    pub fn quote_validator_param(&self) -> proc_macro2::TokenStream {
        let ident = &self.ident;
        if self._type.starts_with("Option<") {
            quote!(#ident)
        } else if COW_TYPE.is_match(&self._type.as_ref()) {
            quote!(self.#ident.as_ref())
        } else if self._type.starts_with('&') || NUMBER_TYPES.contains(&self._type.as_ref()) {
            quote!(self.#ident)
        } else {
            quote!(&self.#ident)
        }
    }
    pub fn quote_validator_field(&self) -> proc_macro2::TokenStream {
        let ident = &self.ident;
        if self._type.starts_with("Option<") || self._type.starts_with("Vec<") {
            quote!(#ident)
        } else if COW_TYPE.is_match(&self._type.as_ref()) {
            quote!(self.#ident.as_ref())
        } else {
            quote!(self.#ident)
        }
    }
    pub fn get_optional_validator_param(&self) -> proc_macro2::TokenStream {
        let ident = &self.ident;
        if self._type.starts_with("Option<&")
            || self._type.starts_with("Option proc_macro2::TokenStream {
        let field_ident = &self.ident;
        let optional_pattern_matched = self.get_optional_validator_param();
        if self._type.starts_with("Option  proc_macro2::TokenStream {
        let field_ident = &self.ident;
        let field_name = &self.name;
        if self._type.starts_with("Vec<") {
            return quote!(
            if !::validator::ValidationErrors::has_error(&result, #field_name) {
                let results: Vec<_> = self.#field_ident.iter().map(|#field_ident| {
                    let mut result = ::std::result::Result::Ok(());
                    #tokens
                    result
                }).collect();
                result = ::validator::ValidationErrors::merge_all(result, #field_name, results);
            });
        }
        tokens
    }
}
/// Quote an actual end-user error creation automatically
fn quote_error(validation: &FieldValidation) -> proc_macro2::TokenStream {
    let code = &validation.code;
    let add_message_quoted = if let Some(ref m) = validation.message {
        quote!(err.message = Some(::std::borrow::Cow::from(#m));)
    } else {
        quote!()
    };
    quote!(
        let mut err = ::validator::ValidationError::new(#code);
        #add_message_quoted
    )
}
pub fn quote_length_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
) -> proc_macro2::TokenStream {
    let field_name = &field_quoter.name;
    let validator_param = field_quoter.quote_validator_param();
    if let Validator::Length { min, max, equal } = validation.validator {
        // Can't interpolate None
        let min_tokens = option_u64_to_tokens(min);
        let max_tokens = option_u64_to_tokens(max);
        let equal_tokens = option_u64_to_tokens(equal);
        let min_err_param_quoted = if let Some(v) = min {
            quote!(err.add_param(::std::borrow::Cow::from("min"), v);)
        } else {
            quote!()
        };
        let max_err_param_quoted = if let Some(v) = max {
            quote!(err.add_param(::std::borrow::Cow::from("max"), v);)
        } else {
            quote!()
        };
        let equal_err_param_quoted = if let Some(v) = equal {
            quote!(err.add_param(::std::borrow::Cow::from("equal"), v);)
        } else {
            quote!()
        };
        let quoted_error = quote_error(&validation);
        let quoted = quote!(
            if !::validator::validate_length(
                ::validator::Validator::Length {
                    min: #min_tokens,
                    max: #max_tokens,
                    equal: #equal_tokens
                },
                #validator_param
            ) {
                #quoted_error
                #min_err_param_quoted
                #max_err_param_quoted
                #equal_err_param_quoted
                err.add_param(::std::borrow::Cow::from("value"), validator_param);
                errors.add(#field_name, err);
            }
        );
        return field_quoter.wrap_if_option(quoted);
    }
    unreachable!()
}
pub fn quote_range_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
) -> proc_macro2::TokenStream {
    let field_name = &field_quoter.name;
    let quoted_ident = field_quoter.quote_validator_param();
    if let Validator::Range { min, max } = validation.validator {
        // Can't interpolate None
        let min_tokens = option_f64_to_tokens(min);
        let max_tokens = option_f64_to_tokens(max);
        let min_err_param_quoted = if let Some(v) = min {
            quote!(err.add_param(::std::borrow::Cow::from("min"), v);)
        } else {
            quote!()
        };
        let max_err_param_quoted = if let Some(v) = max {
            quote!(err.add_param(::std::borrow::Cow::from("max"), v);)
        } else {
            quote!()
        };
        let quoted_error = quote_error(&validation);
        let quoted = quote!(
            if !::validator::validate_range(
                ::validator::Validator::Range {min: #min_tokens, max: #max_tokens},
                #quoted_ident as f64
            ) {
                #quoted_error
                #min_err_param_quoted
                #max_err_param_quoted
                err.add_param(::std::borrow::Cow::from("value"), quoted_ident);
                errors.add(#field_name, err);
            }
        );
        return field_quoter.wrap_if_option(quoted);
    }
    unreachable!()
}
#[cfg(feature = "card")]
pub fn quote_credit_card_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
) -> proc_macro2::TokenStream {
    let field_name = &field_quoter.name;
    let validator_param = field_quoter.quote_validator_param();
    let quoted_error = quote_error(&validation);
    let quoted = quote!(
        if !::validator::validate_credit_card(#validator_param) {
            #quoted_error
            err.add_param(::std::borrow::Cow::from("value"), validator_param);
            errors.add(#field_name, err);
        }
    );
    field_quoter.wrap_if_option(quoted)
}
#[cfg(feature = "phone")]
pub fn quote_phone_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
) -> proc_macro2::TokenStream {
    let field_name = &field_quoter.name;
    let validator_param = field_quoter.quote_validator_param();
    let quoted_error = quote_error(&validation);
    let quoted = quote!(
        if !::validator::validate_phone(#validator_param) {
            #quoted_error
            err.add_param(::std::borrow::Cow::from("value"), validator_param);
            errors.add(#field_name, err);
        }
    );
    field_quoter.wrap_if_option(quoted)
}
#[cfg(feature = "unic")]
pub fn quote_non_control_character_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
) -> proc_macro2::TokenStream {
    let field_name = &field_quoter.name;
    let validator_param = field_quoter.quote_validator_param();
    let quoted_error = quote_error(&validation);
    let quoted = quote!(
        if !::validator::validate_non_control_character(#validator_param) {
            #quoted_error
            err.add_param(::std::borrow::Cow::from("value"), validator_param);
            errors.add(#field_name, err);
        }
    );
    field_quoter.wrap_if_option(quoted)
}
pub fn quote_url_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
) -> proc_macro2::TokenStream {
    let field_name = &field_quoter.name;
    let validator_param = field_quoter.quote_validator_param();
    let quoted_error = quote_error(&validation);
    let quoted = quote!(
        if !::validator::validate_url(#validator_param) {
            #quoted_error
            err.add_param(::std::borrow::Cow::from("value"), validator_param);
            errors.add(#field_name, err);
        }
    );
    field_quoter.wrap_if_option(quoted)
}
pub fn quote_email_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
) -> proc_macro2::TokenStream {
    let field_name = &field_quoter.name;
    let validator_param = field_quoter.quote_validator_param();
    let quoted_error = quote_error(&validation);
    let quoted = quote!(
        if !::validator::validate_email(#validator_param) {
            #quoted_error
            err.add_param(::std::borrow::Cow::from("value"), validator_param);
            errors.add(#field_name, err);
        }
    );
    field_quoter.wrap_if_option(quoted)
}
pub fn quote_must_match_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
) -> proc_macro2::TokenStream {
    let ident = &field_quoter.ident;
    let field_name = &field_quoter.name;
    if let Validator::MustMatch(ref other) = validation.validator {
        let other_ident = syn::Ident::new(other, Span::call_site());
        let quoted_error = quote_error(&validation);
        let quoted = quote!(
            if !::validator::validate_must_match(&self.#ident, &self.#other_ident) {
                #quoted_error
                err.add_param(::std::borrow::Cow::from("value"), &self.#ident);
                err.add_param(::std::borrow::Cow::from("other"), &self.#other_ident);
                errors.add(#field_name, err);
            }
        );
        return field_quoter.wrap_if_option(quoted);
    }
    unreachable!();
}
pub fn quote_custom_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
) -> proc_macro2::TokenStream {
    let field_name = &field_quoter.name;
    let validator_param = field_quoter.quote_validator_param();
    if let Validator::Custom(ref fun) = validation.validator {
        let fn_ident: syn::Path = syn::parse_str(fun).unwrap();
        let add_message_quoted = if let Some(ref m) = validation.message {
            quote!(err.message = Some(::std::borrow::Cow::from(#m));)
        } else {
            quote!()
        };
        let quoted = quote!(
            match #fn_ident(#validator_param) {
                ::std::result::Result::Ok(()) => (),
                ::std::result::Result::Err(mut err) => {
                    #add_message_quoted
                    err.add_param(::std::borrow::Cow::from("value"), validator_param);
                    errors.add(#field_name, err);
                },
            };
        );
        return field_quoter.wrap_if_option(quoted);
    }
    unreachable!();
}
pub fn quote_contains_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
) -> proc_macro2::TokenStream {
    let field_name = &field_quoter.name;
    let validator_param = field_quoter.quote_validator_param();
    if let Validator::Contains(ref needle) = validation.validator {
        let quoted_error = quote_error(&validation);
        let quoted = quote!(
            if !::validator::validate_contains(#validator_param, needle) {
                #quoted_error
                err.add_param(::std::borrow::Cow::from("value"), validator_param);
                err.add_param(::std::borrow::Cow::from("needle"), needle);
                errors.add(#field_name, err);
            }
        );
        return field_quoter.wrap_if_option(quoted);
    }
    unreachable!();
}
pub fn quote_regex_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
) -> proc_macro2::TokenStream {
    let field_name = &field_quoter.name;
    let validator_param = field_quoter.quote_validator_param();
    if let Validator::Regex(ref re) = validation.validator {
        let re_ident: syn::Path = syn::parse_str(re).unwrap();
        let quoted_error = quote_error(&validation);
        let quoted = quote!(
            if !#re_ident.is_match(#validator_param) {
                #quoted_error
                err.add_param(::std::borrow::Cow::from("value"), validator_param);
                errors.add(#field_name, err);
            }
        );
        return field_quoter.wrap_if_option(quoted);
    }
    unreachable!();
}
pub fn quote_nested_validation(field_quoter: &FieldQuoter) -> proc_macro2::TokenStream {
    let field_name = &field_quoter.name;
    let validator_field = field_quoter.quote_validator_field();
    let quoted = quote!(result = ::validator::ValidationErrors::merge(result, #field_name, #validator_field.validate()););
    field_quoter.wrap_if_option(field_quoter.wrap_if_vector(quoted))
}
pub fn quote_field_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
    validations: &mut Vec,
    nested_validations: &mut Vec,
) {
    match validation.validator {
        Validator::Length { .. } => {
            validations.push(quote_length_validation(&field_quoter, validation))
        }
        Validator::Range { .. } => {
            validations.push(quote_range_validation(&field_quoter, validation))
        }
        Validator::Email => validations.push(quote_email_validation(&field_quoter, validation)),
        Validator::Url => validations.push(quote_url_validation(&field_quoter, validation)),
        Validator::MustMatch(_) => {
            validations.push(quote_must_match_validation(&field_quoter, validation))
        }
        Validator::Custom(_) => {
            validations.push(quote_custom_validation(&field_quoter, validation))
        }
        Validator::Contains(_) => {
            validations.push(quote_contains_validation(&field_quoter, validation))
        }
        Validator::Regex(_) => validations.push(quote_regex_validation(&field_quoter, validation)),
        #[cfg(feature = "card")]
        Validator::CreditCard => {
            validations.push(quote_credit_card_validation(&field_quoter, validation))
        }
        #[cfg(feature = "phone")]
        Validator::Phone => validations.push(quote_phone_validation(&field_quoter, validation)),
        Validator::Nested => nested_validations.push(quote_nested_validation(&field_quoter)),
        #[cfg(feature = "unic")]
        Validator::NonControlCharacter => {
            validations.push(quote_non_control_character_validation(&field_quoter, validation))
        }
        Validator::Required | Validator::RequiredNested => {
            validations.push(quote_required_validation(&field_quoter, validation))
        }
    }
}
pub fn quote_schema_validation(validation: Option) -> proc_macro2::TokenStream {
    if let Some(v) = validation {
        let fn_ident = syn::Ident::new(&v.function, Span::call_site());
        let add_message_quoted = if let Some(ref m) = v.message {
            quote!(err.message = Some(::std::borrow::Cow::from(#m));)
        } else {
            quote!()
        };
        let mut_err_token = if v.message.is_some() { quote!(mut) } else { quote!() };
        let quoted = quote!(
            match #fn_ident(self) {
                ::std::result::Result::Ok(()) => (),
                ::std::result::Result::Err(#mut_err_token err) => {
                    #add_message_quoted
                    errors.add("__all__", err);
                },
            };
        );
        if !v.skip_on_field_errors {
            return quoted;
        }
        quote!(
            if errors.is_empty() {
                #quoted
            }
        )
    } else {
        quote!()
    }
}
pub fn quote_required_validation(
    field_quoter: &FieldQuoter,
    validation: &FieldValidation,
) -> proc_macro2::TokenStream {
    let field_name = &field_quoter.name;
    let ident = &field_quoter.ident;
    let validator_param = quote!(&self.#ident);
    let quoted_error = quote_error(&validation);
    let quoted = quote!(
        if !::validator::validate_required(#validator_param) {
            #quoted_error
            err.add_param(::std::borrow::Cow::from("value"), validator_param);
            errors.add(#field_name, err);
        }
    );
    quoted
}