use chalk_ir::{AdtId, TyKind};
use either::Either;
use hir_def::db::DefDatabase;
use project_model::{toolchain_info::QueryConfig, Sysroot};
use rustc_hash::FxHashMap;
use syntax::ToSmolStr;
use test_fixture::WithFixture;
use triomphe::Arc;

use crate::{
    db::HirDatabase,
    layout::{Layout, LayoutError},
    test_db::TestDB,
    Interner, Substitution,
};

mod closure;

fn current_machine_data_layout() -> String {
    project_model::toolchain_info::target_data_layout::get(
        QueryConfig::Rustc(&Sysroot::empty(), &std::env::current_dir().unwrap()),
        None,
        &FxHashMap::default(),
    )
    .unwrap()
}

fn eval_goal(ra_fixture: &str, minicore: &str) -> Result<Arc<Layout>, LayoutError> {
    let target_data_layout = current_machine_data_layout();
    let ra_fixture = format!(
        "//- target_data_layout: {target_data_layout}\n{minicore}//- /main.rs crate:test\n{ra_fixture}",
    );

    let (db, file_ids) = TestDB::with_many_files(&ra_fixture);
    let adt_or_type_alias_id = file_ids
        .into_iter()
        .find_map(|file_id| {
            let module_id = db.module_for_file(file_id.file_id());
            let def_map = module_id.def_map(&db);
            let scope = &def_map[module_id.local_id].scope;
            let adt_or_type_alias_id = scope.declarations().find_map(|x| match x {
                hir_def::ModuleDefId::AdtId(x) => {
                    let name = match x {
                        hir_def::AdtId::StructId(x) => {
                            db.struct_data(x).name.display_no_db(file_id.edition()).to_smolstr()
                        }
                        hir_def::AdtId::UnionId(x) => {
                            db.union_data(x).name.display_no_db(file_id.edition()).to_smolstr()
                        }
                        hir_def::AdtId::EnumId(x) => {
                            db.enum_data(x).name.display_no_db(file_id.edition()).to_smolstr()
                        }
                    };
                    (name == "Goal").then_some(Either::Left(x))
                }
                hir_def::ModuleDefId::TypeAliasId(x) => {
                    let name =
                        db.type_alias_data(x).name.display_no_db(file_id.edition()).to_smolstr();
                    (name == "Goal").then_some(Either::Right(x))
                }
                _ => None,
            })?;
            Some(adt_or_type_alias_id)
        })
        .unwrap();
    let goal_ty = match adt_or_type_alias_id {
        Either::Left(adt_id) => {
            TyKind::Adt(AdtId(adt_id), Substitution::empty(Interner)).intern(Interner)
        }
        Either::Right(ty_id) => {
            db.ty(ty_id.into()).substitute(Interner, &Substitution::empty(Interner))
        }
    };
    db.layout_of_ty(
        goal_ty,
        db.trait_environment(match adt_or_type_alias_id {
            Either::Left(adt) => hir_def::GenericDefId::AdtId(adt),
            Either::Right(ty) => hir_def::GenericDefId::TypeAliasId(ty),
        }),
    )
}

/// A version of `eval_goal` for types that can not be expressed in ADTs, like closures and `impl Trait`
fn eval_expr(ra_fixture: &str, minicore: &str) -> Result<Arc<Layout>, LayoutError> {
    let target_data_layout = current_machine_data_layout();
    let ra_fixture = format!(
        "//- target_data_layout: {target_data_layout}\n{minicore}//- /main.rs crate:test\nfn main(){{let goal = {{{ra_fixture}}};}}",
    );

    let (db, file_id) = TestDB::with_single_file(&ra_fixture);
    let module_id = db.module_for_file(file_id.file_id());
    let def_map = module_id.def_map(&db);
    let scope = &def_map[module_id.local_id].scope;
    let function_id = scope
        .declarations()
        .find_map(|x| match x {
            hir_def::ModuleDefId::FunctionId(x) => {
                let name = db.function_data(x).name.display_no_db(file_id.edition()).to_smolstr();
                (name == "main").then_some(x)
            }
            _ => None,
        })
        .unwrap();
    let hir_body = db.body(function_id.into());
    let b = hir_body
        .bindings
        .iter()
        .find(|x| x.1.name.display_no_db(file_id.edition()).to_smolstr() == "goal")
        .unwrap()
        .0;
    let infer = db.infer(function_id.into());
    let goal_ty = infer.type_of_binding[b].clone();
    db.layout_of_ty(goal_ty, db.trait_environment(function_id.into()))
}

#[track_caller]
fn check_size_and_align(ra_fixture: &str, minicore: &str, size: u64, align: u64) {
    let l = eval_goal(ra_fixture, minicore).unwrap();
    assert_eq!(l.size.bytes(), size, "size mismatch");
    assert_eq!(l.align.abi.bytes(), align, "align mismatch");
}

#[track_caller]
fn check_size_and_align_expr(ra_fixture: &str, minicore: &str, size: u64, align: u64) {
    let l = eval_expr(ra_fixture, minicore).unwrap();
    assert_eq!(l.size.bytes(), size, "size mismatch");
    assert_eq!(l.align.abi.bytes(), align, "align mismatch");
}

#[track_caller]
fn check_fail(ra_fixture: &str, e: LayoutError) {
    let r = eval_goal(ra_fixture, "");
    assert_eq!(r, Err(e));
}

macro_rules! size_and_align {
    (minicore: $($x:tt),*;$($t:tt)*) => {
        {
            #![allow(dead_code)]
            $($t)*
            check_size_and_align(
                stringify!($($t)*),
                &format!("//- minicore: {}\n", stringify!($($x),*)),
                ::std::mem::size_of::<Goal>() as u64,
                ::std::mem::align_of::<Goal>() as u64,
            );
        }
    };
    ($($t:tt)*) => {
        {
            #![allow(dead_code)]
            $($t)*
            check_size_and_align(
                stringify!($($t)*),
                "",
                ::std::mem::size_of::<Goal>() as u64,
                ::std::mem::align_of::<Goal>() as u64,
            );
        }
    };
}

#[macro_export]
macro_rules! size_and_align_expr {
    (minicore: $($x:tt),*; stmts: [$($s:tt)*] $($t:tt)*) => {
        {
            #[allow(dead_code)]
            #[allow(unused_must_use)]
            #[allow(path_statements)]
            {
                $($s)*
                let val = { $($t)* };
                $crate::layout::tests::check_size_and_align_expr(
                    &format!("{{ {} let val = {{ {} }}; val }}", stringify!($($s)*), stringify!($($t)*)),
                    &format!("//- minicore: {}\n", stringify!($($x),*)),
                    ::std::mem::size_of_val(&val) as u64,
                    ::std::mem::align_of_val(&val) as u64,
                );
            }
        }
    };
    ($($t:tt)*) => {
        {
            #[allow(dead_code)]
            {
                let val = { $($t)* };
                $crate::layout::tests::check_size_and_align_expr(
                    stringify!($($t)*),
                    "",
                    ::std::mem::size_of_val(&val) as u64,
                    ::std::mem::align_of_val(&val) as u64,
                );
            }
        }
    };
}

#[test]
fn hello_world() {
    size_and_align! {
        struct Goal(i32);
    }
    size_and_align_expr! {
        2i32
    }
}

#[test]
fn field_order_optimization() {
    size_and_align! {
        struct Goal(u8, i32, u8);
    }
    size_and_align! {
        #[repr(C)]
        struct Goal(u8, i32, u8);
    }
}

#[test]
fn recursive() {
    size_and_align! {
        struct Goal {
            left: &'static Goal,
            right: &'static Goal,
        }
    }
    size_and_align! {
        struct BoxLike<T: ?Sized>(*mut T);
        struct Goal(BoxLike<Goal>);
    }
    check_fail(r#"struct Goal(Goal);"#, LayoutError::RecursiveTypeWithoutIndirection);
    check_fail(
        r#"
        struct Foo<T>(Foo<T>);
        struct Goal(Foo<i32>);
        "#,
        LayoutError::RecursiveTypeWithoutIndirection,
    );
}

#[test]
fn repr_packed() {
    size_and_align! {
        #[repr(packed)]
        struct Goal;
    }
    size_and_align! {
        #[repr(packed(2))]
        struct Goal;
    }
    size_and_align! {
        #[repr(packed(4))]
        struct Goal;
    }
    size_and_align! {
        #[repr(packed)]
        struct Goal(i32);
    }
    size_and_align! {
        #[repr(packed(2))]
        struct Goal(i32);
    }
    size_and_align! {
        #[repr(packed(4))]
        struct Goal(i32);
    }

    check_size_and_align("#[repr(packed(5))] struct Goal(i32);", "", 4, 1);
}

#[test]
fn generic() {
    size_and_align! {
        struct Pair<A, B>(A, B);
        struct Goal(Pair<Pair<i32, u8>, i64>);
    }
    size_and_align! {
        struct X<const N: usize> {
            field1: [i32; N],
            field2: [u8; N],
        }
        struct Goal(X<1000>);
    }
}

#[test]
fn associated_types() {
    size_and_align! {
        trait Tr {
            type Ty;
        }

        impl Tr for i32 {
            type Ty = i64;
        }

        struct Foo<A: Tr>(<A as Tr>::Ty);
        struct Bar<A: Tr>(A::Ty);
        struct Goal(Foo<i32>, Bar<i32>, <i32 as Tr>::Ty);
    }
    check_size_and_align(
        r#"
//- /b/mod.rs crate:b
pub trait Tr {
    type Ty;
}
pub struct Foo<A: Tr>(<A as Tr>::Ty);

//- /a/mod.rs crate:a deps:b
use b::{Tr, Foo};

struct S;
impl Tr for S {
    type Ty = i64;
}
struct Goal(Foo<S>);
        "#,
        "",
        8,
        8,
    );
}

#[test]
fn simd_types() {
    check_size_and_align(
        r#"
            #[repr(simd)]
            struct SimdType(i64, i64);
            struct Goal(SimdType);
        "#,
        "",
        16,
        16,
    );
}

#[test]
fn return_position_impl_trait() {
    size_and_align_expr! {
        trait T {}
        impl T for i32 {}
        impl T for i64 {}
        fn foo() -> impl T { 2i64 }
        foo()
    }
    size_and_align_expr! {
        trait T {}
        impl T for i32 {}
        impl T for i64 {}
        fn foo() -> (impl T, impl T, impl T) { (2i64, 5i32, 7i32) }
        foo()
    }
    size_and_align_expr! {
        minicore: iterators;
        stmts: []
        trait Tr {}
        impl Tr for i32 {}
        fn foo() -> impl Iterator<Item = impl Tr> {
            [1, 2, 3].into_iter()
        }
        let mut iter = foo();
        let item = iter.next();
        (iter, item)
    }
    size_and_align_expr! {
        minicore: future;
        stmts: []
        use core::{future::Future, task::{Poll, Context}, pin::pin};
        use std::{task::Wake, sync::Arc};
        trait Tr {}
        impl Tr for i32 {}
        async fn f() -> impl Tr {
            2
        }
        fn unwrap_fut<T>(inp: impl Future<Output = T>) -> Poll<T> {
            // In a normal test we could use `loop {}` or `panic!()` here,
            // but rustc actually runs this code.
            let pinned = pin!(inp);
            struct EmptyWaker;
            impl Wake for EmptyWaker {
                fn wake(self: Arc<Self>) {
                }
            }
            let waker = Arc::new(EmptyWaker).into();
            let mut context = Context::from_waker(&waker);

            pinned.poll(&mut context)
        }

        unwrap_fut(f())
    }
    size_and_align_expr! {
        struct Foo<T>(T, T, (T, T));
        trait T {}
        impl T for Foo<i32> {}
        impl T for Foo<i64> {}

        fn foo() -> Foo<impl T> { Foo(
            Foo(1i64, 2, (3, 4)),
            Foo(5, 6, (7, 8)),
            (
                Foo(1i64, 2, (3, 4)),
                Foo(5, 6, (7, 8)),
            ),
        ) }
        foo()
    }
}

#[test]
fn unsized_ref() {
    size_and_align! {
        struct S1([u8]);
        struct S2(S1);
        struct S3(i32, str);
        struct S4(u64, S3);
        #[allow(dead_code)]
        struct S5 {
            field1: u8,
            field2: i16,
            field_last: S4,
        }

        struct Goal(&'static S1, &'static S2, &'static S3, &'static S4, &'static S5);
    }
}

#[test]
fn enums() {
    size_and_align! {
        enum Goal {
            Quit,
            Move { x: i32, y: i32 },
            ChangeColor(i32, i32, i32),
        }
    }
}

#[test]
fn primitives() {
    // FIXME(#17451): Add `f16` and `f128` once they are stabilised.
    size_and_align! {
        struct Goal(i32, i128, isize, usize, f32, f64, bool, char);
    }
}

#[test]
fn tuple() {
    size_and_align! {
        struct Goal((), (i32, u64, bool));
    }
}

#[test]
fn non_zero_and_non_null() {
    size_and_align! {
        minicore: non_zero, non_null, option;
        use core::{num::NonZeroU8, ptr::NonNull};
        struct Goal(Option<NonZeroU8>, Option<NonNull<i32>>);
    }
}

#[test]
fn niche_optimization() {
    size_and_align! {
        minicore: option;
        struct Goal(Option<&'static i32>);
    }
    size_and_align! {
        minicore: option;
        struct Goal(Option<Option<bool>>);
    }
}

#[test]
fn const_eval() {
    size_and_align! {
        struct Goal([i32; 2 + 2]);
    }
    size_and_align! {
        const X: usize = 5;
        struct Goal([i32; X]);
    }
    size_and_align! {
        mod foo {
            pub(super) const BAR: usize = 5;
        }
        struct Ar<T>([T; foo::BAR]);
        struct Goal(Ar<Ar<i32>>);
    }
    size_and_align! {
        type Goal = [u8; 2 + 2];
    }
}

#[test]
fn enums_with_discriminants() {
    size_and_align! {
        enum Goal {
            A = 1000,
            B = 2000,
            C = 3000,
        }
    }
    size_and_align! {
        enum Goal {
            A = 254,
            B,
            C, // implicitly becomes 256, so we need two bytes
        }
    }
    size_and_align! {
        enum Goal {
            A = 1, // This one is (perhaps surprisingly) zero sized.
        }
    }
}

#[test]
fn core_mem_discriminant() {
    size_and_align! {
        minicore: discriminant;
        struct S(i32, u64);
        struct Goal(core::mem::Discriminant<S>);
    }
    size_and_align! {
        minicore: discriminant;
        #[repr(u32)]
        enum S {
            A,
            B,
            C,
        }
        struct Goal(core::mem::Discriminant<S>);
    }
    size_and_align! {
        minicore: discriminant;
        enum S {
            A(i32),
            B(i64),
            C(u8),
        }
        struct Goal(core::mem::Discriminant<S>);
    }
    size_and_align! {
        minicore: discriminant;
        #[repr(C, u16)]
        enum S {
            A(i32),
            B(i64) = 200,
            C = 1000,
        }
        struct Goal(core::mem::Discriminant<S>);
    }
}
