using System;
using System.Diagnostics;

namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
{
    static class InstEmitVfpConvert
    {
        public static void Vcvta(CodeGenContext context, uint rd, uint rm, bool op, uint size)
        {
            if (size == 3)
            {
                // F64 -> S32/U32 conversion on SIMD is not supported, so we convert it to a GPR, then insert it back into the SIMD register.

                if (op)
                {
                    InstEmitNeonCommon.EmitScalarUnaryToGprTempF(context, rd, rm, size, 0, context.Arm64Assembler.FcvtasFloat);
                }
                else
                {
                    InstEmitNeonCommon.EmitScalarUnaryToGprTempF(context, rd, rm, size, 0, context.Arm64Assembler.FcvtauFloat);
                }
            }
            else if (op)
            {
                InstEmitNeonCommon.EmitScalarUnaryF(context, rd, rm, size, context.Arm64Assembler.FcvtasS, context.Arm64Assembler.FcvtasSH);
            }
            else
            {
                InstEmitNeonCommon.EmitScalarUnaryF(context, rd, rm, size, context.Arm64Assembler.FcvtauS, context.Arm64Assembler.FcvtauSH);
            }
        }

        public static void Vcvtb(CodeGenContext context, uint rd, uint rm, uint sz, uint op)
        {
            EmitVcvtbVcvtt(context, rd, rm, sz, op, top: false);
        }

        public static void Vcvtm(CodeGenContext context, uint rd, uint rm, bool op, uint size)
        {
            if (size == 3)
            {
                // F64 -> S32/U32 conversion on SIMD is not supported, so we convert it to a GPR, then insert it back into the SIMD register.

                if (op)
                {
                    InstEmitNeonCommon.EmitScalarUnaryToGprTempF(context, rd, rm, size, 0, context.Arm64Assembler.FcvtmsFloat);
                }
                else
                {
                    InstEmitNeonCommon.EmitScalarUnaryToGprTempF(context, rd, rm, size, 0, context.Arm64Assembler.FcvtmuFloat);
                }
            }
            else if (op)
            {
                InstEmitNeonCommon.EmitScalarUnaryF(context, rd, rm, size, context.Arm64Assembler.FcvtmsS, context.Arm64Assembler.FcvtmsSH);
            }
            else
            {
                InstEmitNeonCommon.EmitScalarUnaryF(context, rd, rm, size, context.Arm64Assembler.FcvtmuS, context.Arm64Assembler.FcvtmuSH);
            }
        }

        public static void Vcvtn(CodeGenContext context, uint rd, uint rm, bool op, uint size)
        {
            if (size == 3)
            {
                // F64 -> S32/U32 conversion on SIMD is not supported, so we convert it to a GPR, then insert it back into the SIMD register.

                if (op)
                {
                    InstEmitNeonCommon.EmitScalarUnaryToGprTempF(context, rd, rm, size, 0, context.Arm64Assembler.FcvtnsFloat);
                }
                else
                {
                    InstEmitNeonCommon.EmitScalarUnaryToGprTempF(context, rd, rm, size, 0, context.Arm64Assembler.FcvtnuFloat);
                }
            }
            else if (op)
            {
                InstEmitNeonCommon.EmitScalarUnaryF(context, rd, rm, size, context.Arm64Assembler.FcvtnsS, context.Arm64Assembler.FcvtnsSH);
            }
            else
            {
                InstEmitNeonCommon.EmitScalarUnaryF(context, rd, rm, size, context.Arm64Assembler.FcvtnuS, context.Arm64Assembler.FcvtnuSH);
            }
        }

        public static void Vcvtp(CodeGenContext context, uint rd, uint rm, bool op, uint size)
        {
            if (size == 3)
            {
                // F64 -> S32/U32 conversion on SIMD is not supported, so we convert it to a GPR, then insert it back into the SIMD register.

                if (op)
                {
                    InstEmitNeonCommon.EmitScalarUnaryToGprTempF(context, rd, rm, size, 0, context.Arm64Assembler.FcvtpsFloat);
                }
                else
                {
                    InstEmitNeonCommon.EmitScalarUnaryToGprTempF(context, rd, rm, size, 0, context.Arm64Assembler.FcvtpuFloat);
                }
            }
            else if (op)
            {
                InstEmitNeonCommon.EmitScalarUnaryF(context, rd, rm, size, context.Arm64Assembler.FcvtpsS, context.Arm64Assembler.FcvtpsSH);
            }
            else
            {
                InstEmitNeonCommon.EmitScalarUnaryF(context, rd, rm, size, context.Arm64Assembler.FcvtpuS, context.Arm64Assembler.FcvtpuSH);
            }
        }

        public static void VcvtDs(CodeGenContext context, uint rd, uint rm, uint size)
        {
            bool doubleToSingle = size == 3;

            using ScopedRegister tempRegister = context.RegisterAllocator.AllocateTempSimdRegisterScoped();

            if (doubleToSingle)
            {
                // Double to single.

                using ScopedRegister rmReg = InstEmitNeonCommon.MoveScalarToSide(context, rm, false);

                context.Arm64Assembler.FcvtFloat(tempRegister.Operand, rmReg.Operand, 0, 1);

                InstEmitNeonCommon.InsertResult(context, tempRegister.Operand, rd, true);
            }
            else
            {
                // Single to double.

                using ScopedRegister rmReg = InstEmitNeonCommon.MoveScalarToSide(context, rm, true);

                context.Arm64Assembler.FcvtFloat(tempRegister.Operand, rmReg.Operand, 1, 0);

                InstEmitNeonCommon.InsertResult(context, tempRegister.Operand, rd, false);
            }
        }

        public static void VcvtIv(CodeGenContext context, uint rd, uint rm, bool unsigned, uint size)
        {
            if (size == 3)
            {
                // F64 -> S32/U32 conversion on SIMD is not supported, so we convert it to a GPR, then insert it back into the SIMD register.

                if (unsigned)
                {
                    InstEmitNeonCommon.EmitScalarUnaryToGprTempF(context, rd, rm, size, 0, context.Arm64Assembler.FcvtzuFloatInt);
                }
                else
                {
                    InstEmitNeonCommon.EmitScalarUnaryToGprTempF(context, rd, rm, size, 0, context.Arm64Assembler.FcvtzsFloatInt);
                }
            }
            else
            {
                if (unsigned)
                {
                    InstEmitNeonCommon.EmitScalarUnaryF(context, rd, rm, size, context.Arm64Assembler.FcvtzuIntS, context.Arm64Assembler.FcvtzuIntSH);
                }
                else
                {
                    InstEmitNeonCommon.EmitScalarUnaryF(context, rd, rm, size, context.Arm64Assembler.FcvtzsIntS, context.Arm64Assembler.FcvtzsIntSH);
                }
            }
        }

        public static void VcvtVi(CodeGenContext context, uint rd, uint rm, bool unsigned, uint size)
        {
            if (size == 3)
            {
                // S32/U32 -> F64 conversion on SIMD is not supported, so we convert it to a GPR, then insert it back into the SIMD register.

                if (unsigned)
                {
                    InstEmitNeonCommon.EmitScalarUnaryFromGprTempF(context, rd, rm, size, 0, context.Arm64Assembler.UcvtfFloatInt);
                }
                else
                {
                    InstEmitNeonCommon.EmitScalarUnaryFromGprTempF(context, rd, rm, size, 0, context.Arm64Assembler.ScvtfFloatInt);
                }
            }
            else
            {
                if (unsigned)
                {
                    InstEmitNeonCommon.EmitScalarUnaryF(context, rd, rm, size, context.Arm64Assembler.UcvtfIntS, context.Arm64Assembler.UcvtfIntSH);
                }
                else
                {
                    InstEmitNeonCommon.EmitScalarUnaryF(context, rd, rm, size, context.Arm64Assembler.ScvtfIntS, context.Arm64Assembler.ScvtfIntSH);
                }
            }
        }

        public static void VcvtXv(CodeGenContext context, uint rd, uint imm5, bool sx, uint sf, uint op, bool u)
        {
            Debug.Assert(op >> 1 == 0);

            bool unsigned = u;
            bool toFixed = op == 1;
            uint size = sf;
            uint fbits = Math.Clamp((sx ? 32u : 16u) - imm5, 1, 8u << (int)size);

            if (toFixed)
            {
                if (unsigned)
                {
                    InstEmitNeonCommon.EmitScalarUnaryFixedF(context, rd, rd, fbits, size, is16Bit: false, context.Arm64Assembler.FcvtzuFixS);
                }
                else
                {
                    InstEmitNeonCommon.EmitScalarUnaryFixedF(context, rd, rd, fbits, size, is16Bit: false, context.Arm64Assembler.FcvtzsFixS);
                }
            }
            else
            {
                if (unsigned)
                {
                    InstEmitNeonCommon.EmitScalarUnaryFixedF(context, rd, rd, fbits, size, is16Bit: !sx, context.Arm64Assembler.UcvtfFixS);
                }
                else
                {
                    InstEmitNeonCommon.EmitScalarUnaryFixedF(context, rd, rd, fbits, size, is16Bit: !sx, context.Arm64Assembler.ScvtfFixS);
                }
            }
        }

        public static void VcvtrIv(CodeGenContext context, uint rd, uint rm, uint op, uint size)
        {
            bool unsigned = (op & 1) == 0;

            Debug.Assert(size is 1 or 2 or 3);

            bool singleRegs = size != 3;

            using ScopedRegister rmReg = InstEmitNeonCommon.MoveScalarToSide(context, rm, singleRegs);

            using ScopedRegister tempRegister = InstEmitNeonCommon.PickSimdRegister(context.RegisterAllocator, rmReg);

            // Round using the FPCR rounding mode first, since the FCVTZ instructions will use the round to zero mode.
            context.Arm64Assembler.FrintiFloat(tempRegister.Operand, rmReg.Operand, size ^ 2u);

            if (unsigned)
            {
                if (size == 1)
                {
                    context.Arm64Assembler.FcvtzuIntSH(tempRegister.Operand, tempRegister.Operand);
                }
                else
                {
                    context.Arm64Assembler.FcvtzuIntS(tempRegister.Operand, tempRegister.Operand, size & 1);
                }
            }
            else
            {
                if (size == 1)
                {
                    context.Arm64Assembler.FcvtzsIntSH(tempRegister.Operand, tempRegister.Operand);
                }
                else
                {
                    context.Arm64Assembler.FcvtzsIntS(tempRegister.Operand, tempRegister.Operand, size & 1);
                }
            }

            InstEmitNeonCommon.InsertResult(context, tempRegister.Operand, rd, singleRegs);
        }

        public static void Vcvtt(CodeGenContext context, uint rd, uint rm, uint sz, uint op)
        {
            EmitVcvtbVcvtt(context, rd, rm, sz, op, top: true);
        }

        public static void EmitVcvtbVcvtt(CodeGenContext context, uint rd, uint rm, uint sz, uint op, bool top)
        {
            bool usesDouble = sz == 1;
            bool convertFromHalf = op == 0;

            using ScopedRegister tempRegister = context.RegisterAllocator.AllocateTempSimdRegisterScoped();

            if (convertFromHalf)
            {
                // Half to single/double.

                using ScopedRegister rmReg = InstEmitNeonCommon.Move16BitScalarToSide(context, rm, top);

                context.Arm64Assembler.FcvtFloat(tempRegister.Operand, rmReg.Operand, usesDouble ? 1u : 0u, 3u);

                InstEmitNeonCommon.InsertResult(context, tempRegister.Operand, rd, !usesDouble);
            }
            else
            {
                // Single/double to half.

                using ScopedRegister rmReg = InstEmitNeonCommon.MoveScalarToSide(context, rm, !usesDouble);

                context.Arm64Assembler.FcvtFloat(tempRegister.Operand, rmReg.Operand, 3u, usesDouble ? 1u : 0u);

                InstEmitNeonCommon.Insert16BitResult(context, tempRegister.Operand, rd, top);
            }
        }
    }
}
