using Ryujinx.Graphics.Shader.Decoders;
using Ryujinx.Graphics.Shader.IntermediateRepresentation;
using Ryujinx.Graphics.Shader.Translation;
using System;
using static Ryujinx.Graphics.Shader.Instructions.InstEmitAluHelper;
using static Ryujinx.Graphics.Shader.Instructions.InstEmitHelper;
using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;

namespace Ryujinx.Graphics.Shader.Instructions
{
    static partial class InstEmit
    {
        public static void DsetR(EmitterContext context)
        {
            InstDsetR op = context.GetOp<InstDsetR>();

            var srcA = GetSrcReg(context, op.SrcA, isFP64: true);
            var srcB = GetSrcReg(context, op.SrcB, isFP64: true);

            EmitFset(
                context,
                op.FComp,
                op.Bop,
                srcA,
                srcB,
                op.SrcPred,
                op.SrcPredInv,
                op.Dest,
                op.AbsA,
                op.AbsB,
                op.NegA,
                op.NegB,
                op.BVal,
                op.WriteCC,
                isFP64: true);
        }

        public static void DsetI(EmitterContext context)
        {
            InstDsetI op = context.GetOp<InstDsetI>();

            var srcA = GetSrcReg(context, op.SrcA, isFP64: true);
            var srcB = GetSrcImm(context, Imm20ToFloat(op.Imm20), isFP64: true);

            EmitFset(
                context,
                op.FComp,
                op.Bop,
                srcA,
                srcB,
                op.SrcPred,
                op.SrcPredInv,
                op.Dest,
                op.AbsA,
                op.AbsB,
                op.NegA,
                op.NegB,
                op.BVal,
                op.WriteCC,
                isFP64: true);
        }

        public static void DsetC(EmitterContext context)
        {
            InstDsetC op = context.GetOp<InstDsetC>();

            var srcA = GetSrcReg(context, op.SrcA, isFP64: true);
            var srcB = GetSrcCbuf(context, op.CbufSlot, op.CbufOffset, isFP64: true);

            EmitFset(
                context,
                op.FComp,
                op.Bop,
                srcA,
                srcB,
                op.SrcPred,
                op.SrcPredInv,
                op.Dest,
                op.AbsA,
                op.AbsB,
                op.NegA,
                op.NegB,
                op.BVal,
                op.WriteCC,
                isFP64: true);
        }

        public static void DsetpR(EmitterContext context)
        {
            InstDsetpR op = context.GetOp<InstDsetpR>();

            var srcA = GetSrcReg(context, op.SrcA, isFP64: true);
            var srcB = GetSrcReg(context, op.SrcB, isFP64: true);

            EmitFsetp(
                context,
                op.FComp,
                op.Bop,
                srcA,
                srcB,
                op.SrcPred,
                op.SrcPredInv,
                op.DestPred,
                op.DestPredInv,
                op.AbsA,
                op.AbsB,
                op.NegA,
                op.NegB,
                writeCC: false,
                isFP64: true);
        }

        public static void DsetpI(EmitterContext context)
        {
            InstDsetpI op = context.GetOp<InstDsetpI>();

            var srcA = GetSrcReg(context, op.SrcA, isFP64: true);
            var srcB = GetSrcImm(context, Imm20ToFloat(op.Imm20), isFP64: true);

            EmitFsetp(
                context,
                op.FComp,
                op.Bop,
                srcA,
                srcB,
                op.SrcPred,
                op.SrcPredInv,
                op.DestPred,
                op.DestPredInv,
                op.AbsA,
                op.AbsB,
                op.NegA,
                op.NegB,
                writeCC: false,
                isFP64: true);
        }

        public static void DsetpC(EmitterContext context)
        {
            InstDsetpC op = context.GetOp<InstDsetpC>();

            var srcA = GetSrcReg(context, op.SrcA, isFP64: true);
            var srcB = GetSrcCbuf(context, op.CbufSlot, op.CbufOffset, isFP64: true);

            EmitFsetp(
                context,
                op.FComp,
                op.Bop,
                srcA,
                srcB,
                op.SrcPred,
                op.SrcPredInv,
                op.DestPred,
                op.DestPredInv,
                op.AbsA,
                op.AbsB,
                op.NegA,
                op.NegB,
                writeCC: false,
                isFP64: true);
        }

        public static void FcmpR(EmitterContext context)
        {
            InstFcmpR op = context.GetOp<InstFcmpR>();

            var srcA = GetSrcReg(context, op.SrcA);
            var srcB = GetSrcReg(context, op.SrcB);
            var srcC = GetSrcReg(context, op.SrcC);

            EmitFcmp(context, op.FComp, srcA, srcB, srcC, op.Dest);
        }

        public static void FcmpI(EmitterContext context)
        {
            InstFcmpI op = context.GetOp<InstFcmpI>();

            var srcA = GetSrcReg(context, op.SrcA);
            var srcB = GetSrcImm(context, Imm20ToFloat(op.Imm20));
            var srcC = GetSrcReg(context, op.SrcC);

            EmitFcmp(context, op.FComp, srcA, srcB, srcC, op.Dest);
        }

        public static void FcmpC(EmitterContext context)
        {
            InstFcmpC op = context.GetOp<InstFcmpC>();

            var srcA = GetSrcReg(context, op.SrcA);
            var srcB = GetSrcCbuf(context, op.CbufSlot, op.CbufOffset);
            var srcC = GetSrcReg(context, op.SrcC);

            EmitFcmp(context, op.FComp, srcA, srcB, srcC, op.Dest);
        }

        public static void FcmpRc(EmitterContext context)
        {
            InstFcmpRc op = context.GetOp<InstFcmpRc>();

            var srcA = GetSrcReg(context, op.SrcA);
            var srcB = GetSrcReg(context, op.SrcC);
            var srcC = GetSrcCbuf(context, op.CbufSlot, op.CbufOffset);

            EmitFcmp(context, op.FComp, srcA, srcB, srcC, op.Dest);
        }

        public static void FsetR(EmitterContext context)
        {
            InstFsetR op = context.GetOp<InstFsetR>();

            var srcA = GetSrcReg(context, op.SrcA);
            var srcB = GetSrcReg(context, op.SrcB);

            EmitFset(context, op.FComp, op.Bop, srcA, srcB, op.SrcPred, op.SrcPredInv, op.Dest, op.AbsA, op.AbsB, op.NegA, op.NegB, op.BVal, op.WriteCC);
        }

        public static void FsetC(EmitterContext context)
        {
            InstFsetC op = context.GetOp<InstFsetC>();

            var srcA = GetSrcReg(context, op.SrcA);
            var srcB = GetSrcCbuf(context, op.CbufSlot, op.CbufOffset);

            EmitFset(context, op.FComp, op.Bop, srcA, srcB, op.SrcPred, op.SrcPredInv, op.Dest, op.AbsA, op.AbsB, op.NegA, op.NegB, op.BVal, op.WriteCC);
        }

        public static void FsetI(EmitterContext context)
        {
            InstFsetI op = context.GetOp<InstFsetI>();

            var srcA = GetSrcReg(context, op.SrcA);
            var srcB = GetSrcImm(context, Imm20ToFloat(op.Imm20));

            EmitFset(context, op.FComp, op.Bop, srcA, srcB, op.SrcPred, op.SrcPredInv, op.Dest, op.AbsA, op.AbsB, op.NegA, op.NegB, op.BVal, op.WriteCC);
        }

        public static void FsetpR(EmitterContext context)
        {
            InstFsetpR op = context.GetOp<InstFsetpR>();

            var srcA = GetSrcReg(context, op.SrcA);
            var srcB = GetSrcReg(context, op.SrcB);

            EmitFsetp(
                context,
                op.FComp,
                op.Bop,
                srcA,
                srcB,
                op.SrcPred,
                op.SrcPredInv,
                op.DestPred,
                op.DestPredInv,
                op.AbsA,
                op.AbsB,
                op.NegA,
                op.NegB,
                op.WriteCC);
        }

        public static void FsetpI(EmitterContext context)
        {
            InstFsetpI op = context.GetOp<InstFsetpI>();

            var srcA = GetSrcReg(context, op.SrcA);
            var srcB = GetSrcImm(context, Imm20ToFloat(op.Imm20));

            EmitFsetp(
                context,
                op.FComp,
                op.Bop,
                srcA,
                srcB,
                op.SrcPred,
                op.SrcPredInv,
                op.DestPred,
                op.DestPredInv,
                op.AbsA,
                op.AbsB,
                op.NegA,
                op.NegB,
                op.WriteCC);
        }

        public static void FsetpC(EmitterContext context)
        {
            InstFsetpC op = context.GetOp<InstFsetpC>();

            var srcA = GetSrcReg(context, op.SrcA);
            var srcB = GetSrcCbuf(context, op.CbufSlot, op.CbufOffset);

            EmitFsetp(
                context,
                op.FComp,
                op.Bop,
                srcA,
                srcB,
                op.SrcPred,
                op.SrcPredInv,
                op.DestPred,
                op.DestPredInv,
                op.AbsA,
                op.AbsB,
                op.NegA,
                op.NegB,
                op.WriteCC);
        }

        public static void Hset2R(EmitterContext context)
        {
            InstHset2R op = context.GetOp<InstHset2R>();

            var srcA = GetHalfSrc(context, op.ASwizzle, op.SrcA, op.NegA, op.AbsA);
            var srcB = GetHalfSrc(context, op.BSwizzle, op.SrcB, op.NegB, op.AbsB);

            EmitHset2(context, op.Cmp, op.Bop, srcA, srcB, op.SrcPred, op.SrcPredInv, op.Dest, op.Bval);
        }

        public static void Hset2I(EmitterContext context)
        {
            InstHset2I op = context.GetOp<InstHset2I>();

            var srcA = GetHalfSrc(context, op.ASwizzle, op.SrcA, op.NegA, op.AbsA);
            var srcB = GetHalfSrc(context, op.BimmH0, op.BimmH1);

            EmitHset2(context, op.Cmp, op.Bop, srcA, srcB, op.SrcPred, op.SrcPredInv, op.Dest, op.Bval);
        }

        public static void Hset2C(EmitterContext context)
        {
            InstHset2C op = context.GetOp<InstHset2C>();

            var srcA = GetHalfSrc(context, op.ASwizzle, op.SrcA, op.NegA, op.AbsA);
            var srcB = GetHalfSrc(context, HalfSwizzle.F32, op.CbufSlot, op.CbufOffset, op.NegB, false);

            EmitHset2(context, op.Cmp, op.Bop, srcA, srcB, op.SrcPred, op.SrcPredInv, op.Dest, op.Bval);
        }

        public static void Hsetp2R(EmitterContext context)
        {
            InstHsetp2R op = context.GetOp<InstHsetp2R>();

            var srcA = GetHalfSrc(context, op.ASwizzle, op.SrcA, op.NegA, op.AbsA);
            var srcB = GetHalfSrc(context, op.BSwizzle, op.SrcB, op.NegB, op.AbsB);

            EmitHsetp2(context, op.FComp2, op.Bop, srcA, srcB, op.SrcPred, op.SrcPredInv, op.DestPred, op.DestPredInv, op.HAnd);
        }

        public static void Hsetp2I(EmitterContext context)
        {
            InstHsetp2I op = context.GetOp<InstHsetp2I>();

            var srcA = GetHalfSrc(context, op.ASwizzle, op.SrcA, op.NegA, op.AbsA);
            var srcB = GetHalfSrc(context, op.BimmH0, op.BimmH1);

            EmitHsetp2(context, op.FComp, op.Bop, srcA, srcB, op.SrcPred, op.SrcPredInv, op.DestPred, op.DestPredInv, op.HAnd);
        }

        public static void Hsetp2C(EmitterContext context)
        {
            InstHsetp2C op = context.GetOp<InstHsetp2C>();

            var srcA = GetHalfSrc(context, op.ASwizzle, op.SrcA, op.NegA, op.AbsA);
            var srcB = GetHalfSrc(context, HalfSwizzle.F32, op.CbufSlot, op.CbufOffset, op.NegB, op.AbsB);

            EmitHsetp2(context, op.FComp, op.Bop, srcA, srcB, op.SrcPred, op.SrcPredInv, op.DestPred, op.DestPredInv, op.HAnd);
        }

        private static void EmitFcmp(EmitterContext context, FComp cmpOp, Operand srcA, Operand srcB, Operand srcC, int rd)
        {
            Operand cmpRes = GetFPComparison(context, cmpOp, srcC, ConstF(0));

            Operand res = context.ConditionalSelect(cmpRes, srcA, srcB);

            context.Copy(GetDest(rd), res);
        }

        private static void EmitFset(
            EmitterContext context,
            FComp cmpOp,
            BoolOp logicOp,
            Operand srcA,
            Operand srcB,
            int srcPred,
            bool srcPredInv,
            int rd,
            bool absoluteA,
            bool absoluteB,
            bool negateA,
            bool negateB,
            bool boolFloat,
            bool writeCC,
            bool isFP64 = false)
        {
            Instruction fpType = isFP64 ? Instruction.FP64 : Instruction.FP32;

            srcA = context.FPAbsNeg(srcA, absoluteA, negateA, fpType);
            srcB = context.FPAbsNeg(srcB, absoluteB, negateB, fpType);

            Operand res = GetFPComparison(context, cmpOp, srcA, srcB, fpType);
            Operand pred = GetPredicate(context, srcPred, srcPredInv);

            res = GetPredLogicalOp(context, logicOp, res, pred);

            Operand dest = GetDest(rd);

            if (boolFloat)
            {
                res = context.ConditionalSelect(res, ConstF(1), Const(0));

                context.Copy(dest, res);

                SetFPZnFlags(context, res, writeCC);
            }
            else
            {
                context.Copy(dest, res);

                SetZnFlags(context, res, writeCC, extended: false);
            }
        }

        private static void EmitFsetp(
            EmitterContext context,
            FComp cmpOp,
            BoolOp logicOp,
            Operand srcA,
            Operand srcB,
            int srcPred,
            bool srcPredInv,
            int destPred,
            int destPredInv,
            bool absoluteA,
            bool absoluteB,
            bool negateA,
            bool negateB,
            bool writeCC,
            bool isFP64 = false)
        {
            Instruction fpType = isFP64 ? Instruction.FP64 : Instruction.FP32;

            srcA = context.FPAbsNeg(srcA, absoluteA, negateA, fpType);
            srcB = context.FPAbsNeg(srcB, absoluteB, negateB, fpType);

            Operand p0Res = GetFPComparison(context, cmpOp, srcA, srcB, fpType);
            Operand p1Res = context.BitwiseNot(p0Res);
            Operand pred = GetPredicate(context, srcPred, srcPredInv);

            p0Res = GetPredLogicalOp(context, logicOp, p0Res, pred);
            p1Res = GetPredLogicalOp(context, logicOp, p1Res, pred);

            context.Copy(Register(destPred, RegisterType.Predicate), p0Res);
            context.Copy(Register(destPredInv, RegisterType.Predicate), p1Res);
        }

        private static void EmitHset2(
            EmitterContext context,
            FComp cmpOp,
            BoolOp logicOp,
            Operand[] srcA,
            Operand[] srcB,
            int srcPred,
            bool srcPredInv,
            int rd,
            bool boolFloat)
        {
            Operand[] res = new Operand[2];

            res[0] = GetFPComparison(context, cmpOp, srcA[0], srcB[0]);
            res[1] = GetFPComparison(context, cmpOp, srcA[1], srcB[1]);

            Operand pred = GetPredicate(context, srcPred, srcPredInv);

            res[0] = GetPredLogicalOp(context, logicOp, res[0], pred);
            res[1] = GetPredLogicalOp(context, logicOp, res[1], pred);

            if (boolFloat)
            {
                res[0] = context.ConditionalSelect(res[0], ConstF(1), Const(0));
                res[1] = context.ConditionalSelect(res[1], ConstF(1), Const(0));

                context.Copy(GetDest(rd), context.PackHalf2x16(res[0], res[1]));
            }
            else
            {
                Operand low = context.BitwiseAnd(res[0], Const(0xffff));
                Operand high = context.ShiftLeft(res[1], Const(16));

                Operand packed = context.BitwiseOr(low, high);

                context.Copy(GetDest(rd), packed);
            }
        }

        private static void EmitHsetp2(
            EmitterContext context,
            FComp cmpOp,
            BoolOp logicOp,
            Operand[] srcA,
            Operand[] srcB,
            int srcPred,
            bool srcPredInv,
            int destPred,
            int destPredInv,
            bool hAnd)
        {
            Operand p0Res = GetFPComparison(context, cmpOp, srcA[0], srcB[0]);
            Operand p1Res = GetFPComparison(context, cmpOp, srcA[1], srcB[1]);

            if (hAnd)
            {
                p0Res = context.BitwiseAnd(p0Res, p1Res);
                p1Res = context.BitwiseNot(p0Res);
            }

            Operand pred = GetPredicate(context, srcPred, srcPredInv);

            p0Res = GetPredLogicalOp(context, logicOp, p0Res, pred);
            p1Res = GetPredLogicalOp(context, logicOp, p1Res, pred);

            context.Copy(Register(destPred, RegisterType.Predicate), p0Res);
            context.Copy(Register(destPredInv, RegisterType.Predicate), p1Res);
        }

        private static Operand GetFPComparison(EmitterContext context, FComp cond, Operand srcA, Operand srcB, Instruction fpType = Instruction.FP32)
        {
            Operand res;

            if (cond == FComp.T)
            {
                res = Const(IrConsts.True);
            }
            else if (cond == FComp.F)
            {
                res = Const(IrConsts.False);
            }
            else if (cond is FComp.Nan or FComp.Num)
            {
                res = context.BitwiseOr(context.IsNan(srcA, fpType), context.IsNan(srcB, fpType));

                if (cond == FComp.Num)
                {
                    res = context.BitwiseNot(res);
                }
            }
            else
            {
                var inst = (cond & ~FComp.Nan) switch
                {
                    FComp.Lt => Instruction.CompareLess,
                    FComp.Eq => Instruction.CompareEqual,
                    FComp.Le => Instruction.CompareLessOrEqual,
                    FComp.Gt => Instruction.CompareGreater,
                    FComp.Ne => Instruction.CompareNotEqual,
                    FComp.Ge => Instruction.CompareGreaterOrEqual,
                    _ => throw new ArgumentException($"Unexpected condition \"{cond}\"."),
                };
                res = context.Add(inst | fpType, Local(), srcA, srcB);

                if ((cond & FComp.Nan) != 0)
                {
                    res = context.BitwiseOr(res, context.IsNan(srcA, fpType));
                    res = context.BitwiseOr(res, context.IsNan(srcB, fpType));
                }
            }

            return res;
        }
    }
}
