using ARMeilleure.Decoders;
using ARMeilleure.IntermediateRepresentation;
using ARMeilleure.State;
using ARMeilleure.Translation;
using System;
using System.Diagnostics;

using static ARMeilleure.Instructions.InstEmitHelper;
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;

namespace ARMeilleure.Instructions
{
    static class InstEmitAluHelper
    {
        public static bool ShouldSetFlags(ArmEmitterContext context)
        {
            IOpCode32HasSetFlags op = (IOpCode32HasSetFlags)context.CurrOp;

            if (op.SetFlags == null)
            {
                return !context.IsInIfThenBlock;
            }

            return op.SetFlags.Value;
        }

        public static void EmitNZFlagsCheck(ArmEmitterContext context, Operand d)
        {
            SetFlag(context, PState.NFlag, context.ICompareLess(d, Const(d.Type, 0)));
            SetFlag(context, PState.ZFlag, context.ICompareEqual(d, Const(d.Type, 0)));
        }

        public static void EmitAdcsCCheck(ArmEmitterContext context, Operand n, Operand d)
        {
            // C = (Rd == Rn && CIn) || Rd < Rn
            Operand cIn = GetFlag(PState.CFlag);

            Operand cOut = context.BitwiseAnd(context.ICompareEqual(d, n), cIn);

            cOut = context.BitwiseOr(cOut, context.ICompareLessUI(d, n));

            SetFlag(context, PState.CFlag, cOut);
        }

        public static void EmitAddsCCheck(ArmEmitterContext context, Operand n, Operand d)
        {
            // C = Rd < Rn
            SetFlag(context, PState.CFlag, context.ICompareLessUI(d, n));
        }

        public static void EmitAddsVCheck(ArmEmitterContext context, Operand n, Operand m, Operand d)
        {
            // V = (Rd ^ Rn) & ~(Rn ^ Rm) < 0
            Operand vOut = context.BitwiseExclusiveOr(d, n);

            vOut = context.BitwiseAnd(vOut, context.BitwiseNot(context.BitwiseExclusiveOr(n, m)));

            vOut = context.ICompareLess(vOut, Const(vOut.Type, 0));

            SetFlag(context, PState.VFlag, vOut);
        }

        public static void EmitSbcsCCheck(ArmEmitterContext context, Operand n, Operand m)
        {
            // C = (Rn == Rm && CIn) || Rn > Rm
            Operand cIn = GetFlag(PState.CFlag);

            Operand cOut = context.BitwiseAnd(context.ICompareEqual(n, m), cIn);

            cOut = context.BitwiseOr(cOut, context.ICompareGreaterUI(n, m));

            SetFlag(context, PState.CFlag, cOut);
        }

        public static void EmitSubsCCheck(ArmEmitterContext context, Operand n, Operand m)
        {
            // C = Rn >= Rm
            SetFlag(context, PState.CFlag, context.ICompareGreaterOrEqualUI(n, m));
        }

        public static void EmitSubsVCheck(ArmEmitterContext context, Operand n, Operand m, Operand d)
        {
            // V = (Rd ^ Rn) & (Rn ^ Rm) < 0
            Operand vOut = context.BitwiseExclusiveOr(d, n);

            vOut = context.BitwiseAnd(vOut, context.BitwiseExclusiveOr(n, m));

            vOut = context.ICompareLess(vOut, Const(vOut.Type, 0));

            SetFlag(context, PState.VFlag, vOut);
        }

        public static Operand EmitReverseBits32Op(ArmEmitterContext context, Operand op)
        {
            Debug.Assert(op.Type == OperandType.I32);

            Operand val = context.BitwiseOr(context.ShiftRightUI(context.BitwiseAnd(op, Const(0xaaaaaaaau)), Const(1)),
                                            context.ShiftLeft(context.BitwiseAnd(op, Const(0x55555555u)), Const(1)));

            val = context.BitwiseOr(context.ShiftRightUI(context.BitwiseAnd(val, Const(0xccccccccu)), Const(2)),
                                    context.ShiftLeft(context.BitwiseAnd(val, Const(0x33333333u)), Const(2)));
            val = context.BitwiseOr(context.ShiftRightUI(context.BitwiseAnd(val, Const(0xf0f0f0f0u)), Const(4)),
                                    context.ShiftLeft(context.BitwiseAnd(val, Const(0x0f0f0f0fu)), Const(4)));
            val = context.BitwiseOr(context.ShiftRightUI(context.BitwiseAnd(val, Const(0xff00ff00u)), Const(8)),
                                    context.ShiftLeft(context.BitwiseAnd(val, Const(0x00ff00ffu)), Const(8)));

            return context.BitwiseOr(context.ShiftRightUI(val, Const(16)), context.ShiftLeft(val, Const(16)));
        }

        public static Operand EmitReverseBytes16_64Op(ArmEmitterContext context, Operand op)
        {
            Debug.Assert(op.Type == OperandType.I64);

            return context.BitwiseOr(context.ShiftRightUI(context.BitwiseAnd(op, Const(0xff00ff00ff00ff00ul)), Const(8)),
                                     context.ShiftLeft(context.BitwiseAnd(op, Const(0x00ff00ff00ff00fful)), Const(8)));
        }

        public static Operand EmitReverseBytes16_32Op(ArmEmitterContext context, Operand op)
        {
            Debug.Assert(op.Type == OperandType.I32);

            Operand val = EmitReverseBytes16_64Op(context, context.ZeroExtend32(OperandType.I64, op));

            return context.ConvertI64ToI32(val);
        }

        private static void EmitAluWritePc(ArmEmitterContext context, Operand value)
        {
            Debug.Assert(value.Type == OperandType.I32);

            if (((OpCode32)context.CurrOp).IsThumb)
            {
                bool isReturn = IsA32Return(context);
                if (!isReturn)
                {
                    context.StoreToContext();
                }

                InstEmitFlowHelper.EmitVirtualJump(context, value, isReturn);
            }
            else
            {
                EmitBxWritePc(context, value);
            }
        }

        public static void EmitGenericAluStoreA32(ArmEmitterContext context, int rd, bool setFlags, Operand value)
        {
            Debug.Assert(value.Type == OperandType.I32);

            if (rd == RegisterAlias.Aarch32Pc && setFlags)
            {
                if (setFlags)
                {
                    // TODO: Load SPSR etc.

                    EmitBxWritePc(context, value);
                }
                else
                {
                    EmitAluWritePc(context, value);
                }
            }
            else
            {
                SetIntA32(context, rd, value);
            }
        }

        public static Operand GetAluN(ArmEmitterContext context)
        {
            if (context.CurrOp is IOpCodeAlu op)
            {
                if (op.DataOp == DataOp.Logical || op is IOpCodeAluRs)
                {
                    return GetIntOrZR(context, op.Rn);
                }
                else
                {
                    return GetIntOrSP(context, op.Rn);
                }
            }
            else if (context.CurrOp is IOpCode32Alu op32)
            {
                return GetIntA32(context, op32.Rn);
            }
            else
            {
                throw InvalidOpCodeType(context.CurrOp);
            }
        }

        public static Operand GetAluM(ArmEmitterContext context, bool setCarry = true)
        {
            switch (context.CurrOp)
            {
                // ARM32.
                case IOpCode32AluImm op:
                    {
                        if (ShouldSetFlags(context) && op.IsRotated && setCarry)
                        {
                            SetFlag(context, PState.CFlag, Const((uint)op.Immediate >> 31));
                        }

                        return Const(op.Immediate);
                    }

                case IOpCode32AluImm16 op:
                    return Const(op.Immediate);

                case IOpCode32AluRsImm op:
                    return GetMShiftedByImmediate(context, op, setCarry);
                case IOpCode32AluRsReg op:
                    return GetMShiftedByReg(context, op, setCarry);

                case IOpCode32AluReg op:
                    return GetIntA32(context, op.Rm);

                // ARM64.
                case IOpCodeAluImm op:
                    {
                        if (op.GetOperandType() == OperandType.I32)
                        {
                            return Const((int)op.Immediate);
                        }
                        else
                        {
                            return Const(op.Immediate);
                        }
                    }

                case IOpCodeAluRs op:
                    {
                        Operand value = GetIntOrZR(context, op.Rm);

                        switch (op.ShiftType)
                        {
                            case ShiftType.Lsl:
                                value = context.ShiftLeft(value, Const(op.Shift));
                                break;
                            case ShiftType.Lsr:
                                value = context.ShiftRightUI(value, Const(op.Shift));
                                break;
                            case ShiftType.Asr:
                                value = context.ShiftRightSI(value, Const(op.Shift));
                                break;
                            case ShiftType.Ror:
                                value = context.RotateRight(value, Const(op.Shift));
                                break;
                        }

                        return value;
                    }

                case IOpCodeAluRx op:
                    {
                        Operand value = GetExtendedM(context, op.Rm, op.IntType);

                        value = context.ShiftLeft(value, Const(op.Shift));

                        return value;
                    }

                default:
                    throw InvalidOpCodeType(context.CurrOp);
            }
        }

        private static Exception InvalidOpCodeType(OpCode opCode)
        {
            return new InvalidOperationException($"Invalid OpCode type \"{opCode?.GetType().Name ?? "null"}\".");
        }

        // ARM32 helpers.
        public static Operand GetMShiftedByImmediate(ArmEmitterContext context, IOpCode32AluRsImm op, bool setCarry)
        {
            Operand m = GetIntA32(context, op.Rm);

            int shift = op.Immediate;

            if (shift == 0)
            {
                switch (op.ShiftType)
                {
                    case ShiftType.Lsr:
                        shift = 32;
                        break;
                    case ShiftType.Asr:
                        shift = 32;
                        break;
                    case ShiftType.Ror:
                        shift = 1;
                        break;
                }
            }

            if (shift != 0)
            {
                setCarry &= ShouldSetFlags(context);

                switch (op.ShiftType)
                {
                    case ShiftType.Lsl:
                        m = GetLslC(context, m, setCarry, shift);
                        break;
                    case ShiftType.Lsr:
                        m = GetLsrC(context, m, setCarry, shift);
                        break;
                    case ShiftType.Asr:
                        m = GetAsrC(context, m, setCarry, shift);
                        break;
                    case ShiftType.Ror:
                        if (op.Immediate != 0)
                        {
                            m = GetRorC(context, m, setCarry, shift);
                        }
                        else
                        {
                            m = GetRrxC(context, m, setCarry);
                        }
                        break;
                }
            }

            return m;
        }

        public static int DecodeImmShift(ShiftType shiftType, int shift)
        {
            if (shift == 0)
            {
                switch (shiftType)
                {
                    case ShiftType.Lsr:
                        shift = 32;
                        break;
                    case ShiftType.Asr:
                        shift = 32;
                        break;
                    case ShiftType.Ror:
                        shift = 1;
                        break;
                }
            }

            return shift;
        }

        public static Operand GetMShiftedByReg(ArmEmitterContext context, IOpCode32AluRsReg op, bool setCarry)
        {
            Operand m = GetIntA32(context, op.Rm);
            Operand s = context.ZeroExtend8(OperandType.I32, GetIntA32(context, op.Rs));
            Operand shiftIsZero = context.ICompareEqual(s, Const(0));

            Operand zeroResult = m;
            Operand shiftResult = m;

            setCarry &= ShouldSetFlags(context);

            switch (op.ShiftType)
            {
                case ShiftType.Lsl:
                    shiftResult = EmitLslC(context, m, setCarry, s, shiftIsZero);
                    break;
                case ShiftType.Lsr:
                    shiftResult = EmitLsrC(context, m, setCarry, s, shiftIsZero);
                    break;
                case ShiftType.Asr:
                    shiftResult = EmitAsrC(context, m, setCarry, s, shiftIsZero);
                    break;
                case ShiftType.Ror:
                    shiftResult = EmitRorC(context, m, setCarry, s, shiftIsZero);
                    break;
            }

            return context.ConditionalSelect(shiftIsZero, zeroResult, shiftResult);
        }

        public static void EmitIfHelper(ArmEmitterContext context, Operand boolValue, Action action, bool expected = true)
        {
            Debug.Assert(boolValue.Type == OperandType.I32);

            Operand endLabel = Label();

            if (expected)
            {
                context.BranchIfFalse(endLabel, boolValue);
            }
            else
            {
                context.BranchIfTrue(endLabel, boolValue);
            }

            action();

            context.MarkLabel(endLabel);
        }

        public static Operand EmitLslC(ArmEmitterContext context, Operand m, bool setCarry, Operand shift, Operand shiftIsZero)
        {
            Debug.Assert(m.Type == OperandType.I32 && shift.Type == OperandType.I32 && shiftIsZero.Type == OperandType.I32);

            Operand shiftLarge = context.ICompareGreaterOrEqual(shift, Const(32));
            Operand result = context.ShiftLeft(m, shift);
            if (setCarry)
            {
                EmitIfHelper(context, shiftIsZero, () =>
                {
                    Operand cOut = context.ShiftRightUI(m, context.Subtract(Const(32), shift));

                    cOut = context.BitwiseAnd(cOut, Const(1));
                    cOut = context.ConditionalSelect(context.ICompareGreater(shift, Const(32)), Const(0), cOut);

                    SetFlag(context, PState.CFlag, cOut);
                }, false);
            }

            return context.ConditionalSelect(shiftLarge, Const(0), result);
        }

        public static Operand GetLslC(ArmEmitterContext context, Operand m, bool setCarry, int shift)
        {
            Debug.Assert(m.Type == OperandType.I32);

            if ((uint)shift > 32)
            {
                return GetShiftByMoreThan32(context, setCarry);
            }
            else if (shift == 32)
            {
                if (setCarry)
                {
                    SetCarryMLsb(context, m);
                }

                return Const(0);
            }
            else
            {
                if (setCarry)
                {
                    Operand cOut = context.ShiftRightUI(m, Const(32 - shift));

                    cOut = context.BitwiseAnd(cOut, Const(1));

                    SetFlag(context, PState.CFlag, cOut);
                }

                return context.ShiftLeft(m, Const(shift));
            }
        }

        public static Operand EmitLsrC(ArmEmitterContext context, Operand m, bool setCarry, Operand shift, Operand shiftIsZero)
        {
            Debug.Assert(m.Type == OperandType.I32 && shift.Type == OperandType.I32 && shiftIsZero.Type == OperandType.I32);

            Operand shiftLarge = context.ICompareGreaterOrEqual(shift, Const(32));
            Operand result = context.ShiftRightUI(m, shift);
            if (setCarry)
            {
                EmitIfHelper(context, shiftIsZero, () =>
                {
                    Operand cOut = context.ShiftRightUI(m, context.Subtract(shift, Const(1)));

                    cOut = context.BitwiseAnd(cOut, Const(1));
                    cOut = context.ConditionalSelect(context.ICompareGreater(shift, Const(32)), Const(0), cOut);

                    SetFlag(context, PState.CFlag, cOut);
                }, false);
            }

            return context.ConditionalSelect(shiftLarge, Const(0), result);
        }

        public static Operand GetLsrC(ArmEmitterContext context, Operand m, bool setCarry, int shift)
        {
            Debug.Assert(m.Type == OperandType.I32);

            if ((uint)shift > 32)
            {
                return GetShiftByMoreThan32(context, setCarry);
            }
            else if (shift == 32)
            {
                if (setCarry)
                {
                    SetCarryMMsb(context, m);
                }

                return Const(0);
            }
            else
            {
                if (setCarry)
                {
                    SetCarryMShrOut(context, m, shift);
                }

                return context.ShiftRightUI(m, Const(shift));
            }
        }

        private static Operand GetShiftByMoreThan32(ArmEmitterContext context, bool setCarry)
        {
            if (setCarry)
            {
                SetFlag(context, PState.CFlag, Const(0));
            }

            return Const(0);
        }

        public static Operand EmitAsrC(ArmEmitterContext context, Operand m, bool setCarry, Operand shift, Operand shiftIsZero)
        {
            Debug.Assert(m.Type == OperandType.I32 && shift.Type == OperandType.I32 && shiftIsZero.Type == OperandType.I32);

            Operand l32Result;
            Operand ge32Result;

            Operand less32 = context.ICompareLess(shift, Const(32));

            ge32Result = context.ShiftRightSI(m, Const(31));

            if (setCarry)
            {
                EmitIfHelper(context, context.BitwiseOr(less32, shiftIsZero), () =>
                {
                    SetCarryMLsb(context, ge32Result);
                }, false);
            }

            l32Result = context.ShiftRightSI(m, shift);
            if (setCarry)
            {
                EmitIfHelper(context, context.BitwiseAnd(less32, context.BitwiseNot(shiftIsZero)), () =>
                {
                    Operand cOut = context.ShiftRightUI(m, context.Subtract(shift, Const(1)));

                    cOut = context.BitwiseAnd(cOut, Const(1));

                    SetFlag(context, PState.CFlag, cOut);
                });
            }

            return context.ConditionalSelect(less32, l32Result, ge32Result);
        }

        public static Operand GetAsrC(ArmEmitterContext context, Operand m, bool setCarry, int shift)
        {
            Debug.Assert(m.Type == OperandType.I32);

            if ((uint)shift >= 32)
            {
                m = context.ShiftRightSI(m, Const(31));

                if (setCarry)
                {
                    SetCarryMLsb(context, m);
                }

                return m;
            }
            else
            {
                if (setCarry)
                {
                    SetCarryMShrOut(context, m, shift);
                }

                return context.ShiftRightSI(m, Const(shift));
            }
        }

        public static Operand EmitRorC(ArmEmitterContext context, Operand m, bool setCarry, Operand shift, Operand shiftIsZero)
        {
            Debug.Assert(m.Type == OperandType.I32 && shift.Type == OperandType.I32 && shiftIsZero.Type == OperandType.I32);

            shift = context.BitwiseAnd(shift, Const(0x1f));
            m = context.RotateRight(m, shift);

            if (setCarry)
            {
                EmitIfHelper(context, shiftIsZero, () =>
                {
                    SetCarryMMsb(context, m);
                }, false);
            }

            return m;
        }

        public static Operand GetRorC(ArmEmitterContext context, Operand m, bool setCarry, int shift)
        {
            Debug.Assert(m.Type == OperandType.I32);

            shift &= 0x1f;

            m = context.RotateRight(m, Const(shift));

            if (setCarry)
            {
                SetCarryMMsb(context, m);
            }

            return m;
        }

        public static Operand GetRrxC(ArmEmitterContext context, Operand m, bool setCarry)
        {
            Debug.Assert(m.Type == OperandType.I32);

            // Rotate right by 1 with carry.
            Operand cIn = context.Copy(GetFlag(PState.CFlag));

            if (setCarry)
            {
                SetCarryMLsb(context, m);
            }

            m = context.ShiftRightUI(m, Const(1));

            m = context.BitwiseOr(m, context.ShiftLeft(cIn, Const(31)));

            return m;
        }

        private static void SetCarryMLsb(ArmEmitterContext context, Operand m)
        {
            Debug.Assert(m.Type == OperandType.I32);

            SetFlag(context, PState.CFlag, context.BitwiseAnd(m, Const(1)));
        }

        private static void SetCarryMMsb(ArmEmitterContext context, Operand m)
        {
            Debug.Assert(m.Type == OperandType.I32);

            SetFlag(context, PState.CFlag, context.ShiftRightUI(m, Const(31)));
        }

        private static void SetCarryMShrOut(ArmEmitterContext context, Operand m, int shift)
        {
            Debug.Assert(m.Type == OperandType.I32);

            Operand cOut = context.ShiftRightUI(m, Const(shift - 1));

            cOut = context.BitwiseAnd(cOut, Const(1));

            SetFlag(context, PState.CFlag, cOut);
        }
    }
}
