use rustc_middle::queries::TaggedQueryKey;
use rustc_middle::query::erase::{self, Erased};
use rustc_middle::query::{QueryKey, QueryMode, QueryVTable};
use rustc_middle::ty::TyCtxt;
use rustc_span::Span;

use crate::GetQueryVTable;

macro_rules! define_queries {
    (
        // Note: `$K` and `$V` are unused but present so this can be called by
        // `rustc_with_all_queries`.
        queries {
            $(
                $(#[$attr:meta])*
                fn $name:ident($K:ty) -> $V:ty
                {
                    // Search for (QMODLIST) to find all occurrences of this query modifier list.
                    arena_cache: $arena_cache:literal,
                    cache_on_disk: $cache_on_disk:literal,
                    depth_limit: $depth_limit:literal,
                    desc: $desc:expr,
                    eval_always: $eval_always:literal,
                    feedable: $feedable:literal,
                    handle_cycle_error: $handle_cycle_error:literal,
                    no_force: $no_force:literal,
                    no_hash: $no_hash:literal,
                    returns_error_guaranteed: $returns_error_guaranteed:literal,
                    separate_provide_extern: $separate_provide_extern:literal,
                }
            )*
        }
        // Non-queries are unused here.
        non_queries { $($_:tt)* }
    ) => {
        // This macro expects to be expanded into `crate::query_impl`, which is this file.
        $(
            pub(crate) mod $name {
                use super::*;

                // It seems to be important that every query has its own monomorphic
                // copy of `execute_query_incr` and `execute_query_non_incr`.
                // Trying to inline these wrapper functions into their generic
                // "inner" helpers tends to break `tests/run-make/short-ice`.

                pub(crate) mod execute_query_incr {
                    use super::*;
                    use rustc_middle::queries::$name::{Key, Value};

                    // Adding `__rust_end_short_backtrace` marker to backtraces so that we emit the frames
                    // when `RUST_BACKTRACE=1`, add a new mod with `$name` here is to allow duplicate naming
                    #[inline(never)]
                    pub(crate) fn __rust_end_short_backtrace<'tcx>(
                        tcx: TyCtxt<'tcx>,
                        span: Span,
                        key: Key<'tcx>,
                        mode: QueryMode,
                    ) -> Option<Erased<Value<'tcx>>> {
                        #[cfg(debug_assertions)]
                        let _guard = tracing::span!(tracing::Level::TRACE, stringify!($name), ?key).entered();
                        crate::execution::execute_query_incr_inner(
                            &tcx.query_system.query_vtables.$name,
                            tcx,
                            span,
                            key,
                            mode
                        )
                    }
                }

                pub(crate) mod execute_query_non_incr {
                    use super::*;
                    use rustc_middle::queries::$name::{Key, Value};

                    #[inline(never)]
                    pub(crate) fn __rust_end_short_backtrace<'tcx>(
                        tcx: TyCtxt<'tcx>,
                        span: Span,
                        key: Key<'tcx>,
                        __mode: QueryMode,
                    ) -> Option<Erased<Value<'tcx>>> {
                        Some(crate::execution::execute_query_non_incr_inner(
                            &tcx.query_system.query_vtables.$name,
                            tcx,
                            span,
                            key,
                        ))
                    }
                }

                /// Defines an `invoke_provider` function that calls the query's provider,
                /// to be used as a function pointer in the query's vtable.
                ///
                /// To mark a short-backtrace boundary, the function's actual name
                /// (after demangling) must be `__rust_begin_short_backtrace`.
                mod invoke_provider_fn {
                    use super::*;
                    use rustc_middle::queries::$name::{Key, Value, provided_to_erased};

                    #[inline(never)]
                    pub(crate) fn __rust_begin_short_backtrace<'tcx>(
                        tcx: TyCtxt<'tcx>,
                        key: Key<'tcx>,
                    ) -> Erased<Value<'tcx>> {
                        #[cfg(debug_assertions)]
                        let _guard = tracing::span!(tracing::Level::TRACE, stringify!($name), ?key).entered();

                        // Call the actual provider function for this query.

                        #[cfg($separate_provide_extern)]
                        let provided_value = if let Some(local_key) = key.as_local_key() {
                            (tcx.query_system.local_providers.$name)(tcx, local_key)
                        } else {
                            (tcx.query_system.extern_providers.$name)(tcx, key)
                        };

                        #[cfg(not($separate_provide_extern))]
                        let provided_value = (tcx.query_system.local_providers.$name)(tcx, key);

                        rustc_middle::ty::print::with_reduced_queries!({
                            tracing::trace!(?provided_value);
                        });

                        // Erase the returned value, because `QueryVTable` uses erased values.
                        // For queries with `arena_cache`, this also arena-allocates the value.
                        provided_to_erased(tcx, provided_value)
                    }
                }

                pub(crate) fn make_query_vtable<'tcx>(incremental: bool)
                    -> QueryVTable<'tcx, rustc_middle::queries::$name::Cache<'tcx>>
                {
                    use rustc_middle::queries::$name::Value;
                    QueryVTable {
                        name: stringify!($name),
                        eval_always: $eval_always,
                        depth_limit: $depth_limit,
                        feedable: $feedable,
                        dep_kind: rustc_middle::dep_graph::DepKind::$name,
                        state: Default::default(),
                        cache: Default::default(),
                        cache_on_disk_local: $cache_on_disk,
                        separate_provide_extern: $separate_provide_extern,

                        invoke_provider_fn: self::invoke_provider_fn::__rust_begin_short_backtrace,

                        #[cfg($cache_on_disk)]
                        try_load_from_disk_fn: |tcx, prev_index| {
                            use rustc_middle::queries::$name::{ProvidedValue, provided_to_erased};

                            let loaded_value: ProvidedValue<'tcx> =
                                $crate::plumbing::try_load_from_disk(tcx, prev_index)?;

                            // Arena-alloc the value if appropriate, and erase it.
                            Some(provided_to_erased(tcx, loaded_value))
                        },
                        #[cfg(not($cache_on_disk))]
                        try_load_from_disk_fn: |_tcx, _prev_index| None,

                        #[cfg($handle_cycle_error)]
                        handle_cycle_error_fn: |tcx, key, cycle, err| {
                            use rustc_middle::query::erase::erase_val;

                            erase_val($crate::handle_cycle_error::$name(tcx, key, cycle, err))
                        },
                        #[cfg(not($handle_cycle_error))]
                        handle_cycle_error_fn: |_tcx, _key, _cycle, err| {
                            $crate::handle_cycle_error::default(err)
                        },

                        #[cfg($no_hash)]
                        hash_value_fn: None,
                        #[cfg(not($no_hash))]
                        hash_value_fn: Some(|hcx, erased_value: &erase::Erased<Value<'tcx>>| {
                            let value = erase::restore_val(*erased_value);
                            rustc_middle::dep_graph::hash_result(hcx, &value)
                        }),

                        format_value: |erased_value: &erase::Erased<Value<'tcx>>| {
                            format!("{:?}", erase::restore_val(*erased_value))
                        },
                        create_tagged_key: TaggedQueryKey::$name,
                        execute_query_fn: if incremental {
                            crate::query_impl::$name::execute_query_incr::__rust_end_short_backtrace
                        } else {
                            crate::query_impl::$name::execute_query_non_incr::__rust_end_short_backtrace
                        },
                    }
                }

                /// Marker type that implements [`GetQueryVTable`] for this query.
                pub(crate) enum VTableGetter {}

                impl<'tcx> GetQueryVTable<'tcx> for VTableGetter {
                    type Cache = rustc_middle::queries::$name::Cache<'tcx>;

                    #[inline(always)]
                    fn query_vtable(tcx: TyCtxt<'tcx>) -> &'tcx QueryVTable<'tcx, Self::Cache> {
                        &tcx.query_system.query_vtables.$name
                    }
                }
            }
        )*

        pub(crate) fn make_query_vtables<'tcx>(incremental: bool)
            -> rustc_middle::queries::QueryVTables<'tcx>
        {
            rustc_middle::queries::QueryVTables {
                $(
                    $name: crate::query_impl::$name::make_query_vtable(incremental),
                )*
            }
        }

        /// Given a filter condition (e.g. `ALL` or `CACHE_ON_DISK`), a `tcx`,
        /// and a closure expression that accepts `&QueryVTable`, this macro
        /// calls that closure with each query vtable that satisfies the filter
        /// condition.
        ///
        /// This needs to be a macro, because the vtables can have different
        /// key/value/cache types for different queries.
        ///
        /// This macro's argument syntax is specifically intended to look like
        /// plain Rust code, so that `for_each_query_vtable!(..)` calls will be
        /// formatted by rustfmt.
        ///
        /// To avoid too much nested-macro complication, filter conditions are
        /// implemented by hand as needed.
        macro_rules! for_each_query_vtable {
            // Call with all queries.
            (ALL, $tcx:expr, $closure:expr) => {{
                let tcx: rustc_middle::ty::TyCtxt<'_> = $tcx;
                $(
                    let query: &rustc_middle::query::QueryVTable<'_, _> =
                        &tcx.query_system.query_vtables.$name;
                    $closure(query);
                )*
            }};

            // Only call with queries that can potentially cache to disk.
            //
            // This allows the use of trait bounds that only need to be satisfied
            // by the subset of queries that actually cache to disk.
            (CACHE_ON_DISK, $tcx:expr, $closure:expr) => {{
                let tcx: rustc_middle::ty::TyCtxt<'_> = $tcx;
                $(
                    #[cfg($cache_on_disk)]
                    {
                        let query: &rustc_middle::query::QueryVTable<'_, _> =
                            &tcx.query_system.query_vtables.$name;
                        $closure(query);
                    }
                )*
            }}
        }

        pub(crate) use for_each_query_vtable;
    }
}

rustc_middle::queries::rustc_with_all_queries! { define_queries! }
