use crate::target::Target;
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{
    parse::{Parse, ParseStream},
    parse_quote,
    spanned::Spanned,
    Error, Expr, ExprLit, Lit, LitStr, Pat, Result,
};

pub struct MatchTarget {
    features: LitStr,
    arms: Vec<(Target, Expr)>,
    default_target: Option<Expr>,
}

impl Parse for MatchTarget {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let features = input.parse()?;
        let mut arms = Vec::new();
        let mut default_target = None;

        while !input.is_empty() {
            let arm: syn::Arm = input.parse()?;
            if !arm.attrs.is_empty() {
                return Err(Error::new(arm.attrs[0].span(), "unexpected attribute"));
            }
            let pat = arm.pat;
            if let Some(guard) = arm.guard {
                return Err(Error::new(guard.0.span(), "unexpected guard"));
            }

            fn parse_target(e: &ExprLit) -> Result<Target> {
                if let Lit::Str(s) = &e.lit {
                    Target::parse(s)
                } else {
                    Err(Error::new(e.span(), "expected a string literal"))
                }
            }
            match pat {
                Pat::Lit(lit) => {
                    arms.push((parse_target(&lit)?, *arm.body));
                }
                Pat::Or(or) => {
                    for case in or.cases.iter() {
                        if let Pat::Lit(lit) = case {
                            arms.push((parse_target(lit)?, *arm.body.clone()));
                        } else {
                            return Err(Error::new(case.span(), "expected a string literal"));
                        }
                    }
                }
                Pat::Wild(_) => {
                    default_target = Some(*arm.body);
                    if !input.is_empty() {
                        return Err(Error::new(input.span(), "unreachable targets"));
                    }
                }
                _ => return Err(Error::new(pat.span(), "expected string literal")),
            }
        }

        Ok(MatchTarget {
            features,
            arms,
            default_target,
        })
    }
}

impl ToTokens for MatchTarget {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let mut exprs = Vec::new();
        let mut not_targets = Vec::new();
        for (target, expr) in &self.arms {
            let arch = target.arch();
            let features = target.features();
            let selected_features = &self.features;
            let cfg = crate::cfg::transform(
                parse_quote! { target_features = #selected_features, all(target_arch = #arch #(, target_feature = #features)*) },
            ).unwrap();
            exprs.push(quote! {
                #[cfg(all(#cfg, not(any(#(#not_targets),*))))]
                { #expr }
            });
            not_targets.push(cfg);
        }
        let default_expr = if let Some(expr) = &self.default_target {
            quote! { #expr }
        } else {
            Error::new(Span::call_site(), "no matching target").to_compile_error()
        };

        quote! {
            {
                #(#exprs)*
                #[cfg(not(any(#(#not_targets),*)))]
                #default_expr
            }
        }
        .to_tokens(tokens)
    }
}
