use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{parse::Error, MetaNameValue};

use crate::smart_default::util::find_only;

#[derive(Debug, Clone, Copy)]
pub enum ConversionStrategy {
    NoConversion,
    Into,
}

pub struct DefaultAttr {
    pub code: Option<TokenStream>,
    conversion_strategy: Option<ConversionStrategy>,
}

impl DefaultAttr {
    pub fn find_in_attributes(attrs: &[syn::Attribute]) -> Result<Option<Self>, Error> {
        if let Some(default_attr) =
            find_only(attrs.iter(), |attr| Ok(attr.path().is_ident("default")))?
        {
            match &default_attr.meta {
                syn::Meta::Path(_) => Ok(Some(Self {
                    code: None,
                    conversion_strategy: None,
                })),
                syn::Meta::List(meta) => {
                    // If the meta contains exactly (_code = "...") take the string literal as the
                    // expression
                    if let Ok(ParseCodeHack(code_hack)) = syn::parse(meta.tokens.clone().into()) {
                        Ok(Some(Self {
                            code: Some(code_hack),
                            conversion_strategy: Some(ConversionStrategy::NoConversion),
                        }))
                    } else {
                        Ok(Some(Self {
                            code: Some(meta.tokens.clone()),
                            conversion_strategy: None,
                        }))
                    }
                }
                syn::Meta::NameValue(MetaNameValue { value, .. }) => Ok(Some(Self {
                    code: Some(value.into_token_stream()),
                    conversion_strategy: None,
                })),
            }
        } else {
            Ok(None)
        }
    }

    pub fn conversion_strategy(&self) -> ConversionStrategy {
        if let Some(conversion_strategy) = self.conversion_strategy {
            // Conversion strategy already set
            return conversion_strategy;
        }
        let code = if let Some(code) = &self.code {
            code
        } else {
            // #[default] - so no conversion (`Default::default()` already has the correct type)
            return ConversionStrategy::NoConversion;
        };
        match syn::parse::<syn::Lit>(code.clone().into()) {
            Ok(syn::Lit::Str(_)) | Ok(syn::Lit::ByteStr(_)) => {
                // A string literal - so we need a conversion in case we need to make it a `String`
                return ConversionStrategy::Into;
            }
            _ => {}
        }
        // Not handled by one of the rules, so we don't convert it to avoid causing trouble
        ConversionStrategy::NoConversion
    }
}

struct ParseCodeHack(TokenStream);

impl syn::parse::Parse for ParseCodeHack {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let ident: syn::Ident = input.parse()?;
        if ident != "_code" {
            return Err(Error::new(ident.span(), "Expected `_code`"));
        }
        input.parse::<syn::token::Eq>()?;
        let code: syn::LitStr = input.parse()?;
        let code: TokenStream = code.parse()?;
        Ok(ParseCodeHack(code))
    }
}
