use super::TRANSMUTE_UNDEFINED_REPR;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::ty::is_c_void;
use rustc_hir::Expr;
use rustc_lint::LateContext;
use rustc_middle::ty::{self, GenericArgsRef, IntTy, Ty, UintTy};

#[expect(clippy::too_many_lines)]
pub(super) fn check<'tcx>(
    cx: &LateContext<'tcx>,
    e: &'tcx Expr<'_>,
    from_ty_orig: Ty<'tcx>,
    to_ty_orig: Ty<'tcx>,
) -> bool {
    let mut from_ty = cx.tcx.erase_and_anonymize_regions(from_ty_orig);
    let mut to_ty = cx.tcx.erase_and_anonymize_regions(to_ty_orig);

    while from_ty != to_ty {
        let reduced_tys = reduce_refs(cx, from_ty, to_ty);
        match (reduce_ty(cx, reduced_tys.from_ty), reduce_ty(cx, reduced_tys.to_ty)) {
            // Various forms of type erasure.
            (ReducedTy::TypeErasure { raw_ptr_only: false }, _)
            | (_, ReducedTy::TypeErasure { raw_ptr_only: false }) => return false,
            (ReducedTy::TypeErasure { .. }, _) if reduced_tys.from_raw_ptr => return false,
            (_, ReducedTy::TypeErasure { .. }) if reduced_tys.to_raw_ptr => return false,

            // `Repr(C)` <-> unordered type.
            // If the first field of the `Repr(C)` type matches then the transmute is ok
            (ReducedTy::OrderedFields(Some(from_sub_ty)), ReducedTy::UnorderedFields(to_sub_ty))
            | (ReducedTy::UnorderedFields(from_sub_ty), ReducedTy::OrderedFields(Some(to_sub_ty))) => {
                from_ty = from_sub_ty;
                to_ty = to_sub_ty;
            },
            (ReducedTy::OrderedFields(Some(from_sub_ty)), ReducedTy::Other(to_sub_ty)) if reduced_tys.to_fat_ptr => {
                from_ty = from_sub_ty;
                to_ty = to_sub_ty;
            },
            (ReducedTy::Other(from_sub_ty), ReducedTy::OrderedFields(Some(to_sub_ty))) if reduced_tys.from_fat_ptr => {
                from_ty = from_sub_ty;
                to_ty = to_sub_ty;
            },

            // ptr <-> ptr
            (ReducedTy::Other(from_sub_ty), ReducedTy::Other(to_sub_ty))
                if matches!(from_sub_ty.kind(), ty::Ref(..) | ty::RawPtr(_, _))
                    && matches!(to_sub_ty.kind(), ty::Ref(..) | ty::RawPtr(_, _)) =>
            {
                from_ty = from_sub_ty;
                to_ty = to_sub_ty;
            },

            // fat ptr <-> (*size, *size)
            (ReducedTy::Other(_), ReducedTy::UnorderedFields(to_ty))
                if reduced_tys.from_fat_ptr && is_size_pair(to_ty) =>
            {
                return false;
            },
            (ReducedTy::UnorderedFields(from_ty), ReducedTy::Other(_))
                if reduced_tys.to_fat_ptr && is_size_pair(from_ty) =>
            {
                return false;
            },

            // fat ptr -> some struct | some struct -> fat ptr
            (ReducedTy::Other(_), _) if reduced_tys.from_fat_ptr => {
                span_lint_and_then(
                    cx,
                    TRANSMUTE_UNDEFINED_REPR,
                    e.span,
                    format!("transmute from `{from_ty_orig}` which has an undefined layout"),
                    |diag| {
                        if from_ty_orig.peel_refs() != from_ty.peel_refs() {
                            diag.note(format!("the contained type `{from_ty}` has an undefined layout"));
                        }
                    },
                );
                return true;
            },
            (_, ReducedTy::Other(_)) if reduced_tys.to_fat_ptr => {
                span_lint_and_then(
                    cx,
                    TRANSMUTE_UNDEFINED_REPR,
                    e.span,
                    format!("transmute to `{to_ty_orig}` which has an undefined layout"),
                    |diag| {
                        if to_ty_orig.peel_refs() != to_ty.peel_refs() {
                            diag.note(format!("the contained type `{to_ty}` has an undefined layout"));
                        }
                    },
                );
                return true;
            },

            (ReducedTy::UnorderedFields(from_ty), ReducedTy::UnorderedFields(to_ty)) if from_ty != to_ty => {
                let same_adt_did = if let (ty::Adt(from_def, from_subs), ty::Adt(to_def, to_subs)) =
                    (from_ty.kind(), to_ty.kind())
                    && from_def == to_def
                {
                    if same_except_params(from_subs, to_subs) {
                        return false;
                    }
                    Some(from_def.did())
                } else {
                    None
                };
                span_lint_and_then(
                    cx,
                    TRANSMUTE_UNDEFINED_REPR,
                    e.span,
                    format!(
                        "transmute from `{from_ty_orig}` to `{to_ty_orig}`, both of which have an undefined layout"
                    ),
                    |diag| {
                        if let Some(same_adt_did) = same_adt_did {
                            diag.note(format!(
                                "two instances of the same generic type (`{}`) may have different layouts",
                                cx.tcx.item_name(same_adt_did)
                            ));
                        } else {
                            if from_ty_orig.peel_refs() != from_ty {
                                diag.note(format!("the contained type `{from_ty}` has an undefined layout"));
                            }
                            if to_ty_orig.peel_refs() != to_ty {
                                diag.note(format!("the contained type `{to_ty}` has an undefined layout"));
                            }
                        }
                    },
                );
                return true;
            },
            (
                ReducedTy::UnorderedFields(from_ty),
                ReducedTy::Other(_) | ReducedTy::OrderedFields(..) | ReducedTy::TypeErasure { raw_ptr_only: true },
            ) => {
                span_lint_and_then(
                    cx,
                    TRANSMUTE_UNDEFINED_REPR,
                    e.span,
                    format!("transmute from `{from_ty_orig}` which has an undefined layout"),
                    |diag| {
                        if from_ty_orig.peel_refs() != from_ty {
                            diag.note(format!("the contained type `{from_ty}` has an undefined layout"));
                        }
                    },
                );
                return true;
            },
            (
                ReducedTy::Other(_) | ReducedTy::OrderedFields(..) | ReducedTy::TypeErasure { raw_ptr_only: true },
                ReducedTy::UnorderedFields(to_ty),
            ) => {
                span_lint_and_then(
                    cx,
                    TRANSMUTE_UNDEFINED_REPR,
                    e.span,
                    format!("transmute into `{to_ty_orig}` which has an undefined layout"),
                    |diag| {
                        if to_ty_orig.peel_refs() != to_ty {
                            diag.note(format!("the contained type `{to_ty}` has an undefined layout"));
                        }
                    },
                );
                return true;
            },
            (
                ReducedTy::OrderedFields(..) | ReducedTy::Other(_) | ReducedTy::TypeErasure { raw_ptr_only: true },
                ReducedTy::OrderedFields(..) | ReducedTy::Other(_) | ReducedTy::TypeErasure { raw_ptr_only: true },
            )
            | (ReducedTy::UnorderedFields(_), ReducedTy::UnorderedFields(_)) => {
                break;
            },
        }
    }

    false
}

#[expect(clippy::struct_excessive_bools)]
struct ReducedTys<'tcx> {
    from_ty: Ty<'tcx>,
    to_ty: Ty<'tcx>,
    from_raw_ptr: bool,
    to_raw_ptr: bool,
    from_fat_ptr: bool,
    to_fat_ptr: bool,
}

/// Remove references so long as both types are references.
fn reduce_refs<'tcx>(cx: &LateContext<'tcx>, mut from_ty: Ty<'tcx>, mut to_ty: Ty<'tcx>) -> ReducedTys<'tcx> {
    let mut from_raw_ptr = false;
    let mut to_raw_ptr = false;
    let (from_fat_ptr, to_fat_ptr) = loop {
        break match (from_ty.kind(), to_ty.kind()) {
            (
                &(ty::Ref(_, from_sub_ty, _) | ty::RawPtr(from_sub_ty, _)),
                &(ty::Ref(_, to_sub_ty, _) | ty::RawPtr(to_sub_ty, _)),
            ) => {
                from_raw_ptr = matches!(*from_ty.kind(), ty::RawPtr(_, _));
                from_ty = from_sub_ty;
                to_raw_ptr = matches!(*to_ty.kind(), ty::RawPtr(_, _));
                to_ty = to_sub_ty;
                continue;
            },
            (&(ty::Ref(_, unsized_ty, _) | ty::RawPtr(unsized_ty, _)), _)
                if !unsized_ty.is_sized(cx.tcx, cx.typing_env()) =>
            {
                (true, false)
            },
            (_, &(ty::Ref(_, unsized_ty, _) | ty::RawPtr(unsized_ty, _)))
                if !unsized_ty.is_sized(cx.tcx, cx.typing_env()) =>
            {
                (false, true)
            },
            _ => (false, false),
        };
    };
    ReducedTys {
        from_ty,
        to_ty,
        from_raw_ptr,
        to_raw_ptr,
        from_fat_ptr,
        to_fat_ptr,
    }
}

enum ReducedTy<'tcx> {
    /// The type can be used for type erasure.
    TypeErasure { raw_ptr_only: bool },
    /// The type is a struct containing either zero non-zero sized fields, or multiple non-zero
    /// sized fields with a defined order.
    /// The value is the first non-zero sized type.
    OrderedFields(Option<Ty<'tcx>>),
    /// The type is a struct containing multiple non-zero sized fields with no defined order.
    UnorderedFields(Ty<'tcx>),
    /// Any other type.
    Other(Ty<'tcx>),
}

/// Reduce structs containing a single non-zero sized field to it's contained type.
fn reduce_ty<'tcx>(cx: &LateContext<'tcx>, mut ty: Ty<'tcx>) -> ReducedTy<'tcx> {
    loop {
        ty = cx.tcx.try_normalize_erasing_regions(cx.typing_env(), ty).unwrap_or(ty);
        return match *ty.kind() {
            ty::Array(sub_ty, _) if matches!(sub_ty.kind(), ty::Int(_) | ty::Uint(_)) => {
                ReducedTy::TypeErasure { raw_ptr_only: false }
            },
            ty::Array(sub_ty, _) | ty::Slice(sub_ty) => {
                ty = sub_ty;
                continue;
            },
            ty::Tuple(args) if args.is_empty() => ReducedTy::TypeErasure { raw_ptr_only: false },
            ty::Tuple(args) => {
                let mut iter = args.iter();
                let Some(sized_ty) = iter.find(|&ty| !is_zero_sized_ty(cx, ty)) else {
                    return ReducedTy::OrderedFields(None);
                };
                if iter.all(|ty| is_zero_sized_ty(cx, ty)) {
                    ty = sized_ty;
                    continue;
                }
                ReducedTy::UnorderedFields(ty)
            },
            ty::Adt(def, args) if def.is_struct() => {
                let mut iter = def
                    .non_enum_variant()
                    .fields
                    .iter()
                    .map(|f| cx.tcx.type_of(f.did).instantiate(cx.tcx, args));
                let Some(sized_ty) = iter.find(|&ty| !is_zero_sized_ty(cx, ty)) else {
                    return ReducedTy::TypeErasure { raw_ptr_only: false };
                };
                if iter.all(|ty| is_zero_sized_ty(cx, ty)) {
                    ty = sized_ty;
                    continue;
                }
                if def.repr().inhibit_struct_field_reordering() {
                    ReducedTy::OrderedFields(Some(sized_ty))
                } else {
                    ReducedTy::UnorderedFields(ty)
                }
            },
            ty::Adt(def, _) if def.is_enum() && (def.variants().is_empty() || is_c_void(cx, ty)) => {
                ReducedTy::TypeErasure { raw_ptr_only: false }
            },
            // TODO: Check if the conversion to or from at least one of a union's fields is valid.
            ty::Adt(def, _) if def.is_union() => ReducedTy::TypeErasure { raw_ptr_only: false },
            ty::Foreign(_) | ty::Param(_) => ReducedTy::TypeErasure { raw_ptr_only: false },
            ty::Int(_) | ty::Uint(_) => ReducedTy::TypeErasure { raw_ptr_only: true },
            _ => ReducedTy::Other(ty),
        };
    }
}

fn is_zero_sized_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
    if let Ok(ty) = cx.tcx.try_normalize_erasing_regions(cx.typing_env(), ty)
        && let Ok(layout) = cx.tcx.layout_of(cx.typing_env().as_query_input(ty))
    {
        layout.layout.size().bytes() == 0
    } else {
        false
    }
}

fn is_size_pair(ty: Ty<'_>) -> bool {
    if let ty::Tuple(tys) = *ty.kind()
        && let [ty1, ty2] = &**tys
    {
        matches!(ty1.kind(), ty::Int(IntTy::Isize) | ty::Uint(UintTy::Usize))
            && matches!(ty2.kind(), ty::Int(IntTy::Isize) | ty::Uint(UintTy::Usize))
    } else {
        false
    }
}

fn same_except_params<'tcx>(subs1: GenericArgsRef<'tcx>, subs2: GenericArgsRef<'tcx>) -> bool {
    // TODO: check const parameters as well. Currently this will consider `Array<5>` the same as
    // `Array<6>`
    for (ty1, ty2) in subs1.types().zip(subs2.types()).filter(|(ty1, ty2)| ty1 != ty2) {
        match (ty1.kind(), ty2.kind()) {
            (ty::Param(_), _) | (_, ty::Param(_)) => (),
            (ty::Adt(adt1, subs1), ty::Adt(adt2, subs2)) if adt1 == adt2 && same_except_params(subs1, subs2) => (),
            _ => return false,
        }
    }
    true
}
