using ChocolArm64.Decoders;
using ChocolArm64.IntermediateRepresentation;
using ChocolArm64.State;
using ChocolArm64.Translation;
using System;
using System.Reflection.Emit;

namespace ChocolArm64.Instructions
{
    static class InstEmit32Helper
    {
        public static bool IsThumb(OpCode64 op)
        {
            return op is OpCodeT16;
        }

        public static void EmitLoadFromRegister(ILEmitterCtx context, int register)
        {
            if (register == RegisterAlias.Aarch32Pc)
            {
                OpCode32 op = (OpCode32)context.CurrOp;

                context.EmitLdc_I4((int)op.GetPc());
            }
            else
            {
                context.EmitLdint(InstEmit32Helper.GetRegisterAlias(context.Mode, register));
            }
        }

        public static void EmitStoreToRegister(ILEmitterCtx context, int register)
        {
            if (register == RegisterAlias.Aarch32Pc)
            {
                context.EmitStoreContext();

                EmitBxWritePc(context);
            }
            else
            {
                context.EmitStint(GetRegisterAlias(context.Mode, register));
            }
        }

        public static void EmitBxWritePc(ILEmitterCtx context)
        {
            context.Emit(OpCodes.Dup);

            context.EmitLdc_I4(1);

            context.Emit(OpCodes.And);
            context.Emit(OpCodes.Dup);

            context.EmitStflg((int)PState.TBit);

            ILLabel lblArmMode = new ILLabel();
            ILLabel lblEnd     = new ILLabel();

            context.Emit(OpCodes.Brtrue_S, lblArmMode);

            context.EmitLdc_I4(~1);

            context.Emit(OpCodes.Br_S, lblEnd);

            context.MarkLabel(lblArmMode);

            context.EmitLdc_I4(~3);

            context.MarkLabel(lblEnd);

            context.Emit(OpCodes.And);
            context.Emit(OpCodes.Conv_U8);
            context.Emit(OpCodes.Ret);
        }

        public static int GetRegisterAlias(Aarch32Mode mode, int register)
        {
            //Only registers >= 8 are banked, with registers in the range [8, 12] being
            //banked for the FIQ mode, and registers 13 and 14 being banked for all modes.
            if ((uint)register < 8)
            {
                return register;
            }

            return GetBankedRegisterAlias(mode, register);
        }

        public static int GetBankedRegisterAlias(Aarch32Mode mode, int register)
        {
            switch (register)
            {
                case 8: return mode == Aarch32Mode.Fiq
                    ? RegisterAlias.R8Fiq
                    : RegisterAlias.R8Usr;

                case 9: return mode == Aarch32Mode.Fiq
                    ? RegisterAlias.R9Fiq
                    : RegisterAlias.R9Usr;

                case 10: return mode == Aarch32Mode.Fiq
                    ? RegisterAlias.R10Fiq
                    : RegisterAlias.R10Usr;

                case 11: return mode == Aarch32Mode.Fiq
                    ? RegisterAlias.R11Fiq
                    : RegisterAlias.R11Usr;

                case 12: return mode == Aarch32Mode.Fiq
                    ? RegisterAlias.R12Fiq
                    : RegisterAlias.R12Usr;

                case 13:
                    switch (mode)
                    {
                        case Aarch32Mode.User:
                        case Aarch32Mode.System:     return RegisterAlias.SpUsr;
                        case Aarch32Mode.Fiq:        return RegisterAlias.SpFiq;
                        case Aarch32Mode.Irq:        return RegisterAlias.SpIrq;
                        case Aarch32Mode.Supervisor: return RegisterAlias.SpSvc;
                        case Aarch32Mode.Abort:      return RegisterAlias.SpAbt;
                        case Aarch32Mode.Hypervisor: return RegisterAlias.SpHyp;
                        case Aarch32Mode.Undefined:  return RegisterAlias.SpUnd;

                        default: throw new ArgumentException(nameof(mode));
                    }

                case 14:
                    switch (mode)
                    {
                        case Aarch32Mode.User:
                        case Aarch32Mode.Hypervisor:
                        case Aarch32Mode.System:     return RegisterAlias.LrUsr;
                        case Aarch32Mode.Fiq:        return RegisterAlias.LrFiq;
                        case Aarch32Mode.Irq:        return RegisterAlias.LrIrq;
                        case Aarch32Mode.Supervisor: return RegisterAlias.LrSvc;
                        case Aarch32Mode.Abort:      return RegisterAlias.LrAbt;
                        case Aarch32Mode.Undefined:  return RegisterAlias.LrUnd;

                        default: throw new ArgumentException(nameof(mode));
                    }

                default: throw new ArgumentOutOfRangeException(nameof(register));
            }
        }
    }
}