use std::marker::PhantomData;

use proc_macro2::{Span, TokenStream};
use quote::quote;

/// A generic function that panics when called. Used by `uninitialized()` to
/// return a `fn() -> T` without constructing a `T`.
#[doc(hidden)]
pub fn panicking_uninit_value<T>() -> T {
    panic!("stageleft: tried to use uninitialized free variable")
}

pub struct QuoteTokens {
    pub prelude: Option<TokenStream>,
    pub expr: Option<TokenStream>,
}

pub fn get_final_crate_name(crate_name: &str) -> TokenStream {
    let final_crate = proc_macro_crate::crate_name(crate_name).unwrap_or_else(|_| {
        panic!("Expected consumer `{crate_name}` package to be present in `Cargo.toml`")
    });

    match final_crate {
        proc_macro_crate::FoundCrate::Itself => {
            if std::env::var("CARGO_BIN_NAME").is_ok() {
                let underscored = crate_name.replace('-', "_");
                let underscored_ident = syn::Ident::new(&underscored, Span::call_site());
                quote! { #underscored_ident }
            } else {
                quote! { crate }
            }
        }
        proc_macro_crate::FoundCrate::Name(name) => {
            let ident = syn::Ident::new(&name, Span::call_site());
            quote! { #ident }
        }
    }
}

thread_local! {
    pub(crate) static MACRO_TO_CRATE: std::cell::RefCell<Option<(String, String)>> = const { std::cell::RefCell::new(None) };
}

pub fn set_macro_to_crate(macro_name: impl Into<String>, crate_name: impl Into<String>) {
    MACRO_TO_CRATE.with(|cell| {
        *cell.borrow_mut() = Some((macro_name.into(), crate_name.into()));
    });
}

static TEST_MODULES: std::sync::RwLock<Vec<(&'static str, &'static [&'static str])>> =
    std::sync::RwLock::new(Vec::new());

/// Register test module paths for a crate. Called from ctor in generated code.
pub fn register_test_modules(crate_name: &'static str, modules: &'static [&'static str]) {
    TEST_MODULES.write().unwrap().push((crate_name, modules));
}

/// Check if a module path is inside a test module for the given crate.
pub(crate) fn is_test_module(crate_name: &str, module_path: &str) -> bool {
    let guard = TEST_MODULES.read().unwrap();
    for (name, modules) in guard.iter() {
        if *name == crate_name {
            for test_mod in modules.iter() {
                if module_path == *test_mod || module_path.starts_with(&format!("{test_mod}::")) {
                    return true;
                }
            }
        }
    }
    false
}

pub trait ParseFromLiteral {
    fn parse_from_literal(literal: &syn::Expr) -> Self;
}

macro_rules! impl_parse_from_literal_numeric {
    ($($ty:ty),*) => {
        $(
            impl ParseFromLiteral for $ty {
                fn parse_from_literal(literal: &syn::Expr) -> Self {
                    match literal {
                        syn::Expr::Lit(syn::ExprLit {
                            lit: syn::Lit::Int(lit_int),
                            ..
                        }) => lit_int.base10_parse().unwrap(),
                        _ => panic!("Expected literal"),
                    }
                }
            }
        )*
    };
}

impl ParseFromLiteral for bool {
    fn parse_from_literal(literal: &syn::Expr) -> Self {
        match literal {
            syn::Expr::Lit(syn::ExprLit {
                lit: syn::Lit::Bool(lit_bool),
                ..
            }) => lit_bool.value(),
            _ => panic!("Expected literal"),
        }
    }
}

impl_parse_from_literal_numeric!(i8, i16, i32, i64, i128, isize);
impl_parse_from_literal_numeric!(u8, u16, u32, u64, u128, usize);

/// A variant of `FreeVariableWithContext` that also has a properties type parameter.
/// When `Props = ()`, this is equivalent to `FreeVariableWithContext`.
pub trait FreeVariableWithContextWithProps<Ctx, Props> {
    type O;

    fn to_tokens(self, ctx: &Ctx) -> (QuoteTokens, Props)
    where
        Self: Sized;

    fn uninitialized(&self, _ctx: &Ctx) -> fn() -> Self::O {
        panicking_uninit_value::<Self::O>
    }
}

pub trait FreeVariableWithContext<Ctx>: FreeVariableWithContextWithProps<Ctx, ()> {
    fn to_tokens(self, ctx: &Ctx) -> QuoteTokens
    where
        Self: Sized,
    {
        FreeVariableWithContextWithProps::to_tokens(self, ctx).0
    }

    fn uninitialized(
        &self,
        ctx: &Ctx,
    ) -> fn() -> <Self as FreeVariableWithContextWithProps<Ctx, ()>>::O {
        FreeVariableWithContextWithProps::uninitialized(self, ctx)
    }
}

/// Blanket impl: anything implementing FreeVariableWithContextWithProps<Ctx, ()> also implements FreeVariableWithContext
impl<Ctx, T: FreeVariableWithContextWithProps<Ctx, ()>> FreeVariableWithContext<Ctx> for T {}

pub trait FreeVariable<O>: FreeVariableWithContext<(), O = O> {
    fn to_tokens(self) -> QuoteTokens
    where
        Self: Sized,
    {
        FreeVariableWithContextWithProps::to_tokens(self, &()).0
    }

    fn uninitialized(&self) -> fn() -> O {
        panicking_uninit_value::<O>
    }
}

impl<O, T: FreeVariableWithContext<(), O = O>> FreeVariable<O> for T {}

macro_rules! impl_free_variable_from_literal_numeric {
    ($($ty:ty),*) => {
        $(
            impl<Ctx> FreeVariableWithContextWithProps<Ctx, ()> for $ty {
                type O = $ty;

                fn to_tokens(self, _ctx: &Ctx) -> (QuoteTokens, ()) {
                    (QuoteTokens {
                        prelude: None,
                        expr: Some(quote!(#self))
                    }, ())
                }
            }

            impl<'a, Ctx> crate::QuotedWithContextWithProps<'a, $ty, Ctx, ()> for $ty {}
        )*
    };
}

impl_free_variable_from_literal_numeric!(i8, i16, i32, i64, i128, isize);
impl_free_variable_from_literal_numeric!(u8, u16, u32, u64, u128, usize);

impl<Ctx> FreeVariableWithContextWithProps<Ctx, ()> for &str {
    type O = &'static str;

    fn to_tokens(self, _ctx: &Ctx) -> (QuoteTokens, ()) {
        (
            QuoteTokens {
                prelude: None,
                expr: Some(quote!(#self)),
            },
            (),
        )
    }
}

impl<Ctx> FreeVariableWithContextWithProps<Ctx, ()> for String {
    type O = &'static str;

    fn to_tokens(self, _ctx: &Ctx) -> (QuoteTokens, ()) {
        (
            QuoteTokens {
                prelude: None,
                expr: Some(quote!(#self)),
            },
            (),
        )
    }
}

pub struct Import<T> {
    module_path: &'static str,
    crate_name: &'static str,
    path: &'static str,
    as_name: &'static str,
    _phantom: PhantomData<T>,
}

impl<T> Copy for Import<T> {}
impl<T> Clone for Import<T> {
    fn clone(&self) -> Self {
        *self
    }
}

pub fn create_import<T>(
    module_path: &'static str,
    crate_name: &'static str,
    path: &'static str,
    as_name: &'static str,
    _unused_type_check: T,
) -> Import<T> {
    Import {
        module_path,
        crate_name,
        path,
        as_name,
        _phantom: PhantomData,
    }
}

impl<T, Ctx> FreeVariableWithContextWithProps<Ctx, ()> for Import<T> {
    type O = T;

    fn to_tokens(self, _ctx: &Ctx) -> (QuoteTokens, ()) {
        let final_crate_root = get_final_crate_name(self.crate_name);

        let module_path = syn::parse_str::<syn::Path>(self.module_path).unwrap();
        let parsed = syn::parse_str::<syn::Path>(self.path).unwrap();
        let as_ident = syn::Ident::new(self.as_name, Span::call_site());
        (
            QuoteTokens {
                prelude: Some(quote!(use #final_crate_root::#module_path::#parsed as #as_ident;)),
                expr: None,
            },
            (),
        )
    }
}

pub fn type_hint<O>(v: O) -> O {
    v
}

pub fn fn0_type_hint<'a, O>(f: impl Fn() -> O + 'a) -> impl Fn() -> O + 'a {
    f
}

pub fn fn1_type_hint<'a, I, O>(f: impl Fn(I) -> O + 'a) -> impl Fn(I) -> O + 'a {
    f
}

pub fn fn1_borrow_type_hint<'a, I, O>(f: impl Fn(&I) -> O + 'a) -> impl Fn(&I) -> O + 'a {
    f
}

pub fn fn2_type_hint<'a, I1, I2, O>(f: impl Fn(I1, I2) -> O + 'a) -> impl Fn(I1, I2) -> O + 'a {
    f
}

pub fn fn2_borrow_type_hint<'a, I1, I2, O>(
    f: impl Fn(&I1, &I2) -> O + 'a,
) -> impl Fn(&I1, &I2) -> O + 'a {
    f
}

pub fn fn2_borrow_mut_type_hint<'a, I1, I2, O>(
    f: impl Fn(&mut I1, I2) -> O + 'a,
) -> impl Fn(&mut I1, I2) -> O + 'a {
    f
}

pub fn fnmut0_type_hint<'a, O>(f: impl FnMut() -> O + 'a) -> impl FnMut() -> O + 'a {
    f
}

pub fn fnmut1_type_hint<'a, I, O>(f: impl FnMut(I) -> O + 'a) -> impl FnMut(I) -> O + 'a {
    f
}

pub fn fnmut1_borrow_type_hint<'a, I, O>(f: impl FnMut(&I) -> O + 'a) -> impl FnMut(&I) -> O + 'a {
    f
}

pub fn fnmut2_type_hint<'a, I1, I2, O>(
    f: impl FnMut(I1, I2) -> O + 'a,
) -> impl FnMut(I1, I2) -> O + 'a {
    f
}

pub fn fnmut2_borrow_type_hint<'a, I1, I2, O>(
    f: impl FnMut(&I1, &I2) -> O + 'a,
) -> impl FnMut(&I1, &I2) -> O + 'a {
    f
}
