use rustc_abi::{
    BackendRepr, FieldsShape, HasDataLayout, Primitive, Reg, RegKind, Size, TyAbiInterface,
    TyAndLayout, Variants,
};

use crate::callconv::{ArgAbi, ArgExtension, CastTarget, FnAbi, PassMode, Uniform};
use crate::spec::{HasTargetSpec, LlvmAbi};

#[derive(Copy, Clone)]
enum RegPassKind {
    Float { offset_from_start: Size, ty: Reg },
    Integer { offset_from_start: Size, ty: Reg },
    Unknown,
}

#[derive(Copy, Clone)]
enum FloatConv {
    FloatPair { first_ty: Reg, second_ty_offset_from_start: Size, second_ty: Reg },
    Float(Reg),
    MixedPair { first_ty: Reg, second_ty_offset_from_start: Size, second_ty: Reg },
}

#[derive(Copy, Clone)]
struct CannotUseFpConv;

fn is_loongarch_aggregate<Ty>(arg: &ArgAbi<'_, Ty>) -> bool {
    match arg.layout.backend_repr {
        BackendRepr::SimdVector { .. } => true,
        _ => arg.layout.is_aggregate(),
    }
}

fn should_use_fp_conv_helper<'a, Ty, C>(
    cx: &C,
    arg_layout: &TyAndLayout<'a, Ty>,
    xlen: u64,
    flen: u64,
    field1_kind: &mut RegPassKind,
    field2_kind: &mut RegPassKind,
    offset_from_start: Size,
) -> Result<(), CannotUseFpConv>
where
    Ty: TyAbiInterface<'a, C> + Copy,
{
    match arg_layout.backend_repr {
        BackendRepr::Scalar(scalar) => match scalar.primitive() {
            Primitive::Int(..) | Primitive::Pointer(_) => {
                if arg_layout.size.bits() > xlen {
                    return Err(CannotUseFpConv);
                }
                match (*field1_kind, *field2_kind) {
                    (RegPassKind::Unknown, _) => {
                        *field1_kind = RegPassKind::Integer {
                            offset_from_start,
                            ty: Reg { kind: RegKind::Integer, size: arg_layout.size },
                        };
                    }
                    (RegPassKind::Float { .. }, RegPassKind::Unknown) => {
                        *field2_kind = RegPassKind::Integer {
                            offset_from_start,
                            ty: Reg { kind: RegKind::Integer, size: arg_layout.size },
                        };
                    }
                    _ => return Err(CannotUseFpConv),
                }
            }
            Primitive::Float(_) => {
                if arg_layout.size.bits() > flen {
                    return Err(CannotUseFpConv);
                }
                match (*field1_kind, *field2_kind) {
                    (RegPassKind::Unknown, _) => {
                        *field1_kind = RegPassKind::Float {
                            offset_from_start,
                            ty: Reg { kind: RegKind::Float, size: arg_layout.size },
                        };
                    }
                    (_, RegPassKind::Unknown) => {
                        *field2_kind = RegPassKind::Float {
                            offset_from_start,
                            ty: Reg { kind: RegKind::Float, size: arg_layout.size },
                        };
                    }
                    _ => return Err(CannotUseFpConv),
                }
            }
        },
        BackendRepr::SimdVector { .. } => {
            return Err(CannotUseFpConv);
        }
        BackendRepr::SimdScalableVector { .. } => panic!("scalable vectors are unsupported"),
        BackendRepr::ScalarPair(..) | BackendRepr::Memory { .. } => match arg_layout.fields {
            FieldsShape::Primitive => {
                unreachable!("aggregates can't have `FieldsShape::Primitive`")
            }
            FieldsShape::Union(_) => {
                if !arg_layout.is_zst() {
                    if arg_layout.is_transparent() {
                        let non_1zst_elem = arg_layout.non_1zst_field(cx).expect("not exactly one non-1-ZST field in non-ZST repr(transparent) union").1;
                        return should_use_fp_conv_helper(
                            cx,
                            &non_1zst_elem,
                            xlen,
                            flen,
                            field1_kind,
                            field2_kind,
                            offset_from_start,
                        );
                    }
                    return Err(CannotUseFpConv);
                }
            }
            FieldsShape::Array { count, .. } => {
                for i in 0..count {
                    let elem_layout = arg_layout.field(cx, 0);
                    should_use_fp_conv_helper(
                        cx,
                        &elem_layout,
                        xlen,
                        flen,
                        field1_kind,
                        field2_kind,
                        offset_from_start + elem_layout.size * i,
                    )?;
                }
            }
            FieldsShape::Arbitrary { .. } => {
                match arg_layout.variants {
                    Variants::Multiple { .. } => return Err(CannotUseFpConv),
                    Variants::Single { .. } | Variants::Empty => (),
                }
                for i in arg_layout.fields.index_by_increasing_offset() {
                    let field = arg_layout.field(cx, i);
                    should_use_fp_conv_helper(
                        cx,
                        &field,
                        xlen,
                        flen,
                        field1_kind,
                        field2_kind,
                        offset_from_start + arg_layout.fields.offset(i),
                    )?;
                }
            }
        },
    }
    Ok(())
}

fn should_use_fp_conv<'a, Ty, C>(
    cx: &C,
    arg: &TyAndLayout<'a, Ty>,
    xlen: u64,
    flen: u64,
) -> Option<FloatConv>
where
    Ty: TyAbiInterface<'a, C> + Copy,
{
    let mut field1_kind = RegPassKind::Unknown;
    let mut field2_kind = RegPassKind::Unknown;
    if should_use_fp_conv_helper(
        cx,
        arg,
        xlen,
        flen,
        &mut field1_kind,
        &mut field2_kind,
        Size::ZERO,
    )
    .is_err()
    {
        return None;
    }
    match (field1_kind, field2_kind) {
        (
            RegPassKind::Integer { offset_from_start, .. }
            | RegPassKind::Float { offset_from_start, .. },
            _,
        ) if offset_from_start != Size::ZERO => {
            panic!("type {:?} has a first field with non-zero offset {offset_from_start:?}", arg.ty)
        }
        (
            RegPassKind::Integer { ty: first_ty, .. },
            RegPassKind::Float { offset_from_start, ty: second_ty },
        ) => Some(FloatConv::MixedPair {
            first_ty,
            second_ty_offset_from_start: offset_from_start,
            second_ty,
        }),
        (
            RegPassKind::Float { ty: first_ty, .. },
            RegPassKind::Integer { offset_from_start, ty: second_ty },
        ) => Some(FloatConv::MixedPair {
            first_ty,
            second_ty_offset_from_start: offset_from_start,
            second_ty,
        }),
        (
            RegPassKind::Float { ty: first_ty, .. },
            RegPassKind::Float { offset_from_start, ty: second_ty },
        ) => Some(FloatConv::FloatPair {
            first_ty,
            second_ty_offset_from_start: offset_from_start,
            second_ty,
        }),
        (RegPassKind::Float { ty, .. }, RegPassKind::Unknown) => Some(FloatConv::Float(ty)),
        _ => None,
    }
}

fn classify_ret<'a, Ty, C>(cx: &C, arg: &mut ArgAbi<'a, Ty>, xlen: u64, flen: u64) -> bool
where
    Ty: TyAbiInterface<'a, C> + Copy,
{
    if !arg.layout.is_sized() {
        // Not touching this...
        return false; // I guess? return value of this function is not documented
    }
    if let Some(conv) = should_use_fp_conv(cx, &arg.layout, xlen, flen) {
        match conv {
            FloatConv::Float(f) => {
                arg.cast_to(f);
            }
            FloatConv::FloatPair { first_ty, second_ty_offset_from_start, second_ty } => {
                arg.cast_to(CastTarget::offset_pair(
                    first_ty,
                    second_ty_offset_from_start,
                    second_ty,
                ));
            }
            FloatConv::MixedPair { first_ty, second_ty_offset_from_start, second_ty } => {
                arg.cast_to(CastTarget::offset_pair(
                    first_ty,
                    second_ty_offset_from_start,
                    second_ty,
                ));
            }
        }
        return false;
    }

    let total = arg.layout.size;

    // "Scalars wider than 2✕XLEN are passed by reference and are replaced in
    // the argument list with the address."
    // "Aggregates larger than 2✕XLEN bits are passed by reference and are
    // replaced in the argument list with the address, as are C++ aggregates
    // with nontrivial copy constructors, destructors, or vtables."
    if total.bits() > 2 * xlen {
        // We rely on the LLVM backend lowering code to lower passing a scalar larger than 2*XLEN.
        if is_loongarch_aggregate(arg) {
            arg.make_indirect();
        }
        return true;
    }

    let xlen_reg = match xlen {
        32 => Reg::i32(),
        64 => Reg::i64(),
        _ => unreachable!("Unsupported XLEN: {}", xlen),
    };
    if is_loongarch_aggregate(arg) {
        if total.bits() <= xlen {
            arg.cast_to(xlen_reg);
        } else {
            arg.cast_to(Uniform::new(xlen_reg, Size::from_bits(xlen * 2)));
        }
        return false;
    }

    // "When passed in registers, scalars narrower than XLEN bits are widened
    // according to the sign of their type up to 32 bits, then sign-extended to
    // XLEN bits."
    extend_integer_width(arg, xlen);
    false
}

fn classify_arg<'a, Ty, C>(
    cx: &C,
    arg: &mut ArgAbi<'a, Ty>,
    xlen: u64,
    flen: u64,
    is_vararg: bool,
    avail_gprs: &mut u64,
    avail_fprs: &mut u64,
) where
    Ty: TyAbiInterface<'a, C> + Copy,
{
    if !arg.layout.is_sized() {
        // Not touching this...
        return;
    }
    if arg.layout.pass_indirectly_in_non_rustic_abis(cx) {
        arg.make_indirect();
        *avail_gprs = (*avail_gprs).saturating_sub(1);
        return;
    }
    if !is_vararg {
        match should_use_fp_conv(cx, &arg.layout, xlen, flen) {
            Some(FloatConv::Float(f)) if *avail_fprs >= 1 => {
                *avail_fprs -= 1;
                arg.cast_to(f);
                return;
            }
            Some(FloatConv::FloatPair { first_ty, second_ty_offset_from_start, second_ty })
                if *avail_fprs >= 2 =>
            {
                *avail_fprs -= 2;
                arg.cast_to(CastTarget::offset_pair(
                    first_ty,
                    second_ty_offset_from_start,
                    second_ty,
                ));
                return;
            }
            Some(FloatConv::MixedPair { first_ty, second_ty_offset_from_start, second_ty })
                if *avail_fprs >= 1 && *avail_gprs >= 1 =>
            {
                *avail_gprs -= 1;
                *avail_fprs -= 1;
                arg.cast_to(CastTarget::offset_pair(
                    first_ty,
                    second_ty_offset_from_start,
                    second_ty,
                ));
                return;
            }
            _ => (),
        }
    }

    let total = arg.layout.size;
    let align = arg.layout.align.bits();

    // "Scalars wider than 2✕XLEN are passed by reference and are replaced in
    // the argument list with the address."
    // "Aggregates larger than 2✕XLEN bits are passed by reference and are
    // replaced in the argument list with the address, as are C++ aggregates
    // with nontrivial copy constructors, destructors, or vtables."
    if total.bits() > 2 * xlen {
        // We rely on the LLVM backend lowering code to lower passing a scalar larger than 2*XLEN.
        if is_loongarch_aggregate(arg) {
            arg.make_indirect();
        }
        if *avail_gprs >= 1 {
            *avail_gprs -= 1;
        }
        return;
    }

    let double_xlen_reg = match xlen {
        32 => Reg::i64(),
        64 => Reg::i128(),
        _ => unreachable!("Unsupported XLEN: {}", xlen),
    };

    let xlen_reg = match xlen {
        32 => Reg::i32(),
        64 => Reg::i64(),
        _ => unreachable!("Unsupported XLEN: {}", xlen),
    };

    if total.bits() > xlen {
        let align_regs = align > xlen;
        if is_loongarch_aggregate(arg) {
            arg.cast_to(Uniform::new(
                if align_regs { double_xlen_reg } else { xlen_reg },
                Size::from_bits(xlen * 2),
            ));
        }
        if align_regs && is_vararg {
            *avail_gprs -= *avail_gprs % 2;
        }
        if *avail_gprs >= 2 {
            *avail_gprs -= 2;
        } else {
            *avail_gprs = 0;
        }
        return;
    } else if is_loongarch_aggregate(arg) {
        arg.cast_to(xlen_reg);
        if *avail_gprs >= 1 {
            *avail_gprs -= 1;
        }
        return;
    }

    // "When passed in registers, scalars narrower than XLEN bits are widened
    // according to the sign of their type up to 32 bits, then sign-extended to
    // XLEN bits."
    if *avail_gprs >= 1 {
        extend_integer_width(arg, xlen);
        *avail_gprs -= 1;
    }
}

fn extend_integer_width<Ty>(arg: &mut ArgAbi<'_, Ty>, xlen: u64) {
    if let BackendRepr::Scalar(scalar) = arg.layout.backend_repr
        && let Primitive::Int(i, _) = scalar.primitive()
        && i.size().bits() == 32
        && xlen > 32
        && let PassMode::Direct(ref mut attrs) = arg.mode
    {
        // 32-bit integers are always sign-extended
        attrs.ext(ArgExtension::Sext);
        return;
    }

    arg.extend_integer_width_to(xlen);
}

pub(crate) fn compute_abi_info<'a, Ty, C>(cx: &C, fn_abi: &mut FnAbi<'a, Ty>)
where
    Ty: TyAbiInterface<'a, C> + Copy,
    C: HasDataLayout + HasTargetSpec,
{
    let xlen = cx.data_layout().pointer_size().bits();
    let flen = match &cx.target_spec().llvm_abiname {
        LlvmAbi::Ilp32f | LlvmAbi::Lp64f => 32,
        LlvmAbi::Ilp32d | LlvmAbi::Lp64d => 64,
        _ => 0,
    };

    let mut avail_gprs = 8;
    let mut avail_fprs = 8;

    if !fn_abi.ret.is_ignore() && classify_ret(cx, &mut fn_abi.ret, xlen, flen) {
        avail_gprs -= 1;
    }

    for (i, arg) in fn_abi.args.iter_mut().enumerate() {
        if arg.is_ignore() {
            continue;
        }
        classify_arg(
            cx,
            arg,
            xlen,
            flen,
            i >= fn_abi.fixed_count as usize,
            &mut avail_gprs,
            &mut avail_fprs,
        );
    }
}

pub(crate) fn compute_rust_abi_info<'a, Ty, C>(cx: &C, fn_abi: &mut FnAbi<'a, Ty>)
where
    Ty: TyAbiInterface<'a, C> + Copy,
    C: HasDataLayout + HasTargetSpec,
{
    let grlen = cx.data_layout().pointer_size().bits();

    for arg in fn_abi.args.iter_mut() {
        if arg.is_ignore() {
            continue;
        }

        // LLVM integers types do not differentiate between signed or unsigned integers.
        // Some LoongArch instructions do not have a `.w` suffix version, they use all the
        // GRLEN bits. By explicitly setting the `signext` or `zeroext` attribute
        // according to signedness to avoid unnecessary integer extending instructions.
        //
        // This is similar to the RISC-V case, see
        // https://github.com/rust-lang/rust/issues/114508 for details.
        extend_integer_width(arg, grlen);
    }
}
