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

using static ARMeilleure.Instructions.InstEmitHelper;
using static ARMeilleure.IntermediateRepresentation.OperandHelper;

namespace ARMeilleure.Instructions
{
    static partial class InstEmit
    {
        [Flags]
        private enum AccessType
        {
            None      = 0,
            Ordered   = 1,
            Exclusive = 2,
            OrderedEx = Ordered | Exclusive
        }

        public static void Clrex(ArmEmitterContext context)
        {
            context.Call(new _Void(NativeInterface.ClearExclusive));
        }

        public static void Dmb(ArmEmitterContext context) => EmitBarrier(context);
        public static void Dsb(ArmEmitterContext context) => EmitBarrier(context);

        public static void Ldar(ArmEmitterContext context)  => EmitLdr(context, AccessType.Ordered);
        public static void Ldaxr(ArmEmitterContext context) => EmitLdr(context, AccessType.OrderedEx);
        public static void Ldxr(ArmEmitterContext context)  => EmitLdr(context, AccessType.Exclusive);
        public static void Ldxp(ArmEmitterContext context)  => EmitLdp(context, AccessType.Exclusive);
        public static void Ldaxp(ArmEmitterContext context) => EmitLdp(context, AccessType.OrderedEx);

        private static void EmitLdr(ArmEmitterContext context, AccessType accType)
        {
            EmitLoadEx(context, accType, pair: false);
        }

        private static void EmitLdp(ArmEmitterContext context, AccessType accType)
        {
            EmitLoadEx(context, accType, pair: true);
        }

        private static void EmitLoadEx(ArmEmitterContext context, AccessType accType, bool pair)
        {
            OpCodeMemEx op = (OpCodeMemEx)context.CurrOp;

            bool ordered   = (accType & AccessType.Ordered)   != 0;
            bool exclusive = (accType & AccessType.Exclusive) != 0;

            if (ordered)
            {
                EmitBarrier(context);
            }

            Operand address = context.Copy(GetIntOrSP(context, op.Rn));

            if (pair)
            {
                // Exclusive loads should be atomic. For pairwise loads, we need to
                // read all the data at once. For a 32-bits pairwise load, we do a
                // simple 64-bits load, for a 128-bits load, we need to call a special
                // method to read 128-bits atomically.
                if (op.Size == 2)
                {
                    Operand value = EmitLoad(context, address, exclusive, 3);

                    Operand valueLow = context.ConvertI64ToI32(value);

                    valueLow = context.ZeroExtend32(OperandType.I64, valueLow);

                    Operand valueHigh = context.ShiftRightUI(value, Const(32));

                    SetIntOrZR(context, op.Rt,  valueLow);
                    SetIntOrZR(context, op.Rt2, valueHigh);
                }
                else if (op.Size == 3)
                {
                    Operand value = EmitLoad(context, address, exclusive, 4);

                    Operand valueLow  = context.VectorExtract(OperandType.I64, value, 0);
                    Operand valueHigh = context.VectorExtract(OperandType.I64, value, 1);

                    SetIntOrZR(context, op.Rt,  valueLow);
                    SetIntOrZR(context, op.Rt2, valueHigh);
                }
                else
                {
                    throw new InvalidOperationException($"Invalid load size of {1 << op.Size} bytes.");
                }
            }
            else
            {
                // 8, 16, 32 or 64-bits (non-pairwise) load.
                Operand value = EmitLoad(context, address, exclusive, op.Size);

                SetIntOrZR(context, op.Rt, value);
            }
        }

        private static Operand EmitLoad(
            ArmEmitterContext context,
            Operand address,
            bool exclusive,
            int size)
        {
            Delegate fallbackMethodDlg = null;

            if (exclusive)
            {
                switch (size)
                {
                    case 0: fallbackMethodDlg = new _U8_U64  (NativeInterface.ReadByteExclusive);      break;
                    case 1: fallbackMethodDlg = new _U16_U64 (NativeInterface.ReadUInt16Exclusive);    break;
                    case 2: fallbackMethodDlg = new _U32_U64 (NativeInterface.ReadUInt32Exclusive);    break;
                    case 3: fallbackMethodDlg = new _U64_U64 (NativeInterface.ReadUInt64Exclusive);    break;
                    case 4: fallbackMethodDlg = new _V128_U64(NativeInterface.ReadVector128Exclusive); break;
                }
            }
            else
            {
                switch (size)
                {
                    case 0: fallbackMethodDlg = new _U8_U64  (NativeInterface.ReadByte);      break;
                    case 1: fallbackMethodDlg = new _U16_U64 (NativeInterface.ReadUInt16);    break;
                    case 2: fallbackMethodDlg = new _U32_U64 (NativeInterface.ReadUInt32);    break;
                    case 3: fallbackMethodDlg = new _U64_U64 (NativeInterface.ReadUInt64);    break;
                    case 4: fallbackMethodDlg = new _V128_U64(NativeInterface.ReadVector128); break;
                }
            }

            return context.Call(fallbackMethodDlg, address);
        }

        public static void Pfrm(ArmEmitterContext context)
        {
            // Memory Prefetch, execute as no-op.
        }

        public static void Stlr(ArmEmitterContext context)  => EmitStr(context, AccessType.Ordered);
        public static void Stlxr(ArmEmitterContext context) => EmitStr(context, AccessType.OrderedEx);
        public static void Stxr(ArmEmitterContext context)  => EmitStr(context, AccessType.Exclusive);
        public static void Stxp(ArmEmitterContext context)  => EmitStp(context, AccessType.Exclusive);
        public static void Stlxp(ArmEmitterContext context) => EmitStp(context, AccessType.OrderedEx);

        private static void EmitStr(ArmEmitterContext context, AccessType accType)
        {
            EmitStoreEx(context, accType, pair: false);
        }

        private static void EmitStp(ArmEmitterContext context, AccessType accType)
        {
            EmitStoreEx(context, accType, pair: true);
        }

        private static void EmitStoreEx(ArmEmitterContext context, AccessType accType, bool pair)
        {
            OpCodeMemEx op = (OpCodeMemEx)context.CurrOp;

            bool ordered   = (accType & AccessType.Ordered)   != 0;
            bool exclusive = (accType & AccessType.Exclusive) != 0;

            if (ordered)
            {
                EmitBarrier(context);
            }

            Operand address = context.Copy(GetIntOrSP(context, op.Rn));

            Operand t = GetIntOrZR(context, op.Rt);

            Operand s = null;

            if (pair)
            {
                Debug.Assert(op.Size == 2 || op.Size == 3, "Invalid size for pairwise store.");

                Operand t2 = GetIntOrZR(context, op.Rt2);

                Operand value;

                if (op.Size == 2)
                {
                    value = context.BitwiseOr(t, context.ShiftLeft(t2, Const(32)));
                }
                else /* if (op.Size == 3) */
                {
                    value = context.VectorInsert(context.VectorZero(), t,  0);
                    value = context.VectorInsert(value,                t2, 1);
                }

                s = EmitStore(context, address, value, exclusive, op.Size + 1);
            }
            else
            {
                s = EmitStore(context, address, t, exclusive, op.Size);
            }

            if (s != null)
            {
                // This is only needed for exclusive stores. The function returns 0
                // when the store is successful, and 1 otherwise.
                SetIntOrZR(context, op.Rs, s);
            }
        }

        private static Operand EmitStore(
            ArmEmitterContext context,
            Operand address,
            Operand value,
            bool exclusive,
            int size)
        {
            if (size < 3)
            {
                value = context.ConvertI64ToI32(value);
            }

            Delegate fallbackMethodDlg = null;

            if (exclusive)
            {
                switch (size)
                {
                    case 0: fallbackMethodDlg = new _S32_U64_U8  (NativeInterface.WriteByteExclusive);      break;
                    case 1: fallbackMethodDlg = new _S32_U64_U16 (NativeInterface.WriteUInt16Exclusive);    break;
                    case 2: fallbackMethodDlg = new _S32_U64_U32 (NativeInterface.WriteUInt32Exclusive);    break;
                    case 3: fallbackMethodDlg = new _S32_U64_U64 (NativeInterface.WriteUInt64Exclusive);    break;
                    case 4: fallbackMethodDlg = new _S32_U64_V128(NativeInterface.WriteVector128Exclusive); break;
                }

                return context.Call(fallbackMethodDlg, address, value);
            }
            else
            {
                switch (size)
                {
                    case 0: fallbackMethodDlg = new _Void_U64_U8  (NativeInterface.WriteByte);      break;
                    case 1: fallbackMethodDlg = new _Void_U64_U16 (NativeInterface.WriteUInt16);    break;
                    case 2: fallbackMethodDlg = new _Void_U64_U32 (NativeInterface.WriteUInt32);    break;
                    case 3: fallbackMethodDlg = new _Void_U64_U64 (NativeInterface.WriteUInt64);    break;
                    case 4: fallbackMethodDlg = new _Void_U64_V128(NativeInterface.WriteVector128); break;
                }

                context.Call(fallbackMethodDlg, address, value);

                return null;
            }
        }

        private static void EmitBarrier(ArmEmitterContext context)
        {
            // Note: This barrier is most likely not necessary, and probably
            // doesn't make any difference since we need to do a ton of stuff
            // (software MMU emulation) to read or write anything anyway.
        }
    }
}