//
// Author:
//   Jb Evain (jbevain@gmail.com)
//
// Copyright (c) 2008 - 2015 Jb Evain
// Copyright (c) 2008 - 2011 Novell, Inc.
//
// Licensed under the MIT/X11 license.
//

using MonoFN.Cecil.Cil;
using System;

namespace MonoFN.Cecil.Rocks
{
#if UNITY_EDITOR
    public
#endif
        static class MethodBodyRocks
    {
        public static void SimplifyMacros(this MethodBody self)
        {
            if (self == null)
                throw new ArgumentNullException("self");

            foreach (Instruction instruction in self.Instructions)
            {
                if (instruction.OpCode.OpCodeType != OpCodeType.Macro)
                    continue;

                switch (instruction.OpCode.Code)
                {
                    case Code.Ldarg_0:
                        ExpandMacro(instruction, OpCodes.Ldarg, self.GetParameter(0));
                        break;
                    case Code.Ldarg_1:
                        ExpandMacro(instruction, OpCodes.Ldarg, self.GetParameter(1));
                        break;
                    case Code.Ldarg_2:
                        ExpandMacro(instruction, OpCodes.Ldarg, self.GetParameter(2));
                        break;
                    case Code.Ldarg_3:
                        ExpandMacro(instruction, OpCodes.Ldarg, self.GetParameter(3));
                        break;
                    case Code.Ldloc_0:
                        ExpandMacro(instruction, OpCodes.Ldloc, self.Variables[0]);
                        break;
                    case Code.Ldloc_1:
                        ExpandMacro(instruction, OpCodes.Ldloc, self.Variables[1]);
                        break;
                    case Code.Ldloc_2:
                        ExpandMacro(instruction, OpCodes.Ldloc, self.Variables[2]);
                        break;
                    case Code.Ldloc_3:
                        ExpandMacro(instruction, OpCodes.Ldloc, self.Variables[3]);
                        break;
                    case Code.Stloc_0:
                        ExpandMacro(instruction, OpCodes.Stloc, self.Variables[0]);
                        break;
                    case Code.Stloc_1:
                        ExpandMacro(instruction, OpCodes.Stloc, self.Variables[1]);
                        break;
                    case Code.Stloc_2:
                        ExpandMacro(instruction, OpCodes.Stloc, self.Variables[2]);
                        break;
                    case Code.Stloc_3:
                        ExpandMacro(instruction, OpCodes.Stloc, self.Variables[3]);
                        break;
                    case Code.Ldarg_S:
                        instruction.OpCode = OpCodes.Ldarg;
                        break;
                    case Code.Ldarga_S:
                        instruction.OpCode = OpCodes.Ldarga;
                        break;
                    case Code.Starg_S:
                        instruction.OpCode = OpCodes.Starg;
                        break;
                    case Code.Ldloc_S:
                        instruction.OpCode = OpCodes.Ldloc;
                        break;
                    case Code.Ldloca_S:
                        instruction.OpCode = OpCodes.Ldloca;
                        break;
                    case Code.Stloc_S:
                        instruction.OpCode = OpCodes.Stloc;
                        break;
                    case Code.Ldc_I4_M1:
                        ExpandMacro(instruction, OpCodes.Ldc_I4, -1);
                        break;
                    case Code.Ldc_I4_0:
                        ExpandMacro(instruction, OpCodes.Ldc_I4, 0);
                        break;
                    case Code.Ldc_I4_1:
                        ExpandMacro(instruction, OpCodes.Ldc_I4, 1);
                        break;
                    case Code.Ldc_I4_2:
                        ExpandMacro(instruction, OpCodes.Ldc_I4, 2);
                        break;
                    case Code.Ldc_I4_3:
                        ExpandMacro(instruction, OpCodes.Ldc_I4, 3);
                        break;
                    case Code.Ldc_I4_4:
                        ExpandMacro(instruction, OpCodes.Ldc_I4, 4);
                        break;
                    case Code.Ldc_I4_5:
                        ExpandMacro(instruction, OpCodes.Ldc_I4, 5);
                        break;
                    case Code.Ldc_I4_6:
                        ExpandMacro(instruction, OpCodes.Ldc_I4, 6);
                        break;
                    case Code.Ldc_I4_7:
                        ExpandMacro(instruction, OpCodes.Ldc_I4, 7);
                        break;
                    case Code.Ldc_I4_8:
                        ExpandMacro(instruction, OpCodes.Ldc_I4, 8);
                        break;
                    case Code.Ldc_I4_S:
                        ExpandMacro(instruction, OpCodes.Ldc_I4, (int)(sbyte)instruction.Operand);
                        break;
                    case Code.Br_S:
                        instruction.OpCode = OpCodes.Br;
                        break;
                    case Code.Brfalse_S:
                        instruction.OpCode = OpCodes.Brfalse;
                        break;
                    case Code.Brtrue_S:
                        instruction.OpCode = OpCodes.Brtrue;
                        break;
                    case Code.Beq_S:
                        instruction.OpCode = OpCodes.Beq;
                        break;
                    case Code.Bge_S:
                        instruction.OpCode = OpCodes.Bge;
                        break;
                    case Code.Bgt_S:
                        instruction.OpCode = OpCodes.Bgt;
                        break;
                    case Code.Ble_S:
                        instruction.OpCode = OpCodes.Ble;
                        break;
                    case Code.Blt_S:
                        instruction.OpCode = OpCodes.Blt;
                        break;
                    case Code.Bne_Un_S:
                        instruction.OpCode = OpCodes.Bne_Un;
                        break;
                    case Code.Bge_Un_S:
                        instruction.OpCode = OpCodes.Bge_Un;
                        break;
                    case Code.Bgt_Un_S:
                        instruction.OpCode = OpCodes.Bgt_Un;
                        break;
                    case Code.Ble_Un_S:
                        instruction.OpCode = OpCodes.Ble_Un;
                        break;
                    case Code.Blt_Un_S:
                        instruction.OpCode = OpCodes.Blt_Un;
                        break;
                    case Code.Leave_S:
                        instruction.OpCode = OpCodes.Leave;
                        break;
                }
            }
        }

        private static void ExpandMacro(Instruction instruction, OpCode opcode, object operand)
        {
            instruction.OpCode = opcode;
            instruction.Operand = operand;
        }

        private static void MakeMacro(Instruction instruction, OpCode opcode)
        {
            instruction.OpCode = opcode;
            instruction.Operand = null;
        }

        public static void Optimize(this MethodBody self)
        {
            if (self == null)
                throw new ArgumentNullException("self");

            OptimizeLongs(self);
            OptimizeMacros(self);
        }

        private static void OptimizeLongs(this MethodBody self)
        {
            for (int i = 0; i < self.Instructions.Count; i++)
            {
                Instruction instruction = self.Instructions[i];
                if (instruction.OpCode.Code != Code.Ldc_I8)
                    continue;
                long l = (long)instruction.Operand;
                if (l >= int.MaxValue || l <= int.MinValue)
                    continue;
                ExpandMacro(instruction, OpCodes.Ldc_I4, (int)l);
                self.Instructions.Insert(++i, Instruction.Create(OpCodes.Conv_I8));
            }
        }

        public static void OptimizeMacros(this MethodBody self)
        {
            if (self == null)
                throw new ArgumentNullException("self");

            MethodDefinition method = self.Method;

            foreach (Instruction instruction in self.Instructions)
            {
                int index;
                switch (instruction.OpCode.Code)
                {
                    case Code.Ldarg:
                        index = ((ParameterDefinition)instruction.Operand).Index;
                        if (index == -1 && instruction.Operand == self.ThisParameter)
                            index = 0;
                        else if (method.HasThis)
                            index++;

                        switch (index)
                        {
                            case 0:
                                MakeMacro(instruction, OpCodes.Ldarg_0);
                                break;
                            case 1:
                                MakeMacro(instruction, OpCodes.Ldarg_1);
                                break;
                            case 2:
                                MakeMacro(instruction, OpCodes.Ldarg_2);
                                break;
                            case 3:
                                MakeMacro(instruction, OpCodes.Ldarg_3);
                                break;
                            default:
                                if (index < 256)
                                    ExpandMacro(instruction, OpCodes.Ldarg_S, instruction.Operand);
                                break;
                        }
                        break;
                    case Code.Ldloc:
                        index = ((VariableDefinition)instruction.Operand).Index;
                        switch (index)
                        {
                            case 0:
                                MakeMacro(instruction, OpCodes.Ldloc_0);
                                break;
                            case 1:
                                MakeMacro(instruction, OpCodes.Ldloc_1);
                                break;
                            case 2:
                                MakeMacro(instruction, OpCodes.Ldloc_2);
                                break;
                            case 3:
                                MakeMacro(instruction, OpCodes.Ldloc_3);
                                break;
                            default:
                                if (index < 256)
                                    ExpandMacro(instruction, OpCodes.Ldloc_S, instruction.Operand);
                                break;
                        }
                        break;
                    case Code.Stloc:
                        index = ((VariableDefinition)instruction.Operand).Index;
                        switch (index)
                        {
                            case 0:
                                MakeMacro(instruction, OpCodes.Stloc_0);
                                break;
                            case 1:
                                MakeMacro(instruction, OpCodes.Stloc_1);
                                break;
                            case 2:
                                MakeMacro(instruction, OpCodes.Stloc_2);
                                break;
                            case 3:
                                MakeMacro(instruction, OpCodes.Stloc_3);
                                break;
                            default:
                                if (index < 256)
                                    ExpandMacro(instruction, OpCodes.Stloc_S, instruction.Operand);
                                break;
                        }
                        break;
                    case Code.Ldarga:
                        index = ((ParameterDefinition)instruction.Operand).Index;
                        if (index == -1 && instruction.Operand == self.ThisParameter)
                            index = 0;
                        else if (method.HasThis)
                            index++;
                        if (index < 256)
                            ExpandMacro(instruction, OpCodes.Ldarga_S, instruction.Operand);
                        break;
                    case Code.Ldloca:
                        if (((VariableDefinition)instruction.Operand).Index < 256)
                            ExpandMacro(instruction, OpCodes.Ldloca_S, instruction.Operand);
                        break;
                    case Code.Ldc_I4:
                        int i = (int)instruction.Operand;
                        switch (i)
                        {
                            case -1:
                                MakeMacro(instruction, OpCodes.Ldc_I4_M1);
                                break;
                            case 0:
                                MakeMacro(instruction, OpCodes.Ldc_I4_0);
                                break;
                            case 1:
                                MakeMacro(instruction, OpCodes.Ldc_I4_1);
                                break;
                            case 2:
                                MakeMacro(instruction, OpCodes.Ldc_I4_2);
                                break;
                            case 3:
                                MakeMacro(instruction, OpCodes.Ldc_I4_3);
                                break;
                            case 4:
                                MakeMacro(instruction, OpCodes.Ldc_I4_4);
                                break;
                            case 5:
                                MakeMacro(instruction, OpCodes.Ldc_I4_5);
                                break;
                            case 6:
                                MakeMacro(instruction, OpCodes.Ldc_I4_6);
                                break;
                            case 7:
                                MakeMacro(instruction, OpCodes.Ldc_I4_7);
                                break;
                            case 8:
                                MakeMacro(instruction, OpCodes.Ldc_I4_8);
                                break;
                            default:
                                if (i >= -128 && i < 128)
                                    ExpandMacro(instruction, OpCodes.Ldc_I4_S, (sbyte)i);
                                break;
                        }
                        break;
                }
            }

            OptimizeBranches(self);
        }

        private static void OptimizeBranches(MethodBody body)
        {
            ComputeOffsets(body);

            foreach (Instruction instruction in body.Instructions)
            {
                if (instruction.OpCode.OperandType != OperandType.InlineBrTarget)
                    continue;

                if (OptimizeBranch(instruction))
                    ComputeOffsets(body);
            }
        }

        private static bool OptimizeBranch(Instruction instruction)
        {
            int offset = ((Instruction)instruction.Operand).Offset - (instruction.Offset + instruction.OpCode.Size + 4);
            if (!(offset >= -128 && offset <= 127))
                return false;

            switch (instruction.OpCode.Code)
            {
                case Code.Br:
                    instruction.OpCode = OpCodes.Br_S;
                    break;
                case Code.Brfalse:
                    instruction.OpCode = OpCodes.Brfalse_S;
                    break;
                case Code.Brtrue:
                    instruction.OpCode = OpCodes.Brtrue_S;
                    break;
                case Code.Beq:
                    instruction.OpCode = OpCodes.Beq_S;
                    break;
                case Code.Bge:
                    instruction.OpCode = OpCodes.Bge_S;
                    break;
                case Code.Bgt:
                    instruction.OpCode = OpCodes.Bgt_S;
                    break;
                case Code.Ble:
                    instruction.OpCode = OpCodes.Ble_S;
                    break;
                case Code.Blt:
                    instruction.OpCode = OpCodes.Blt_S;
                    break;
                case Code.Bne_Un:
                    instruction.OpCode = OpCodes.Bne_Un_S;
                    break;
                case Code.Bge_Un:
                    instruction.OpCode = OpCodes.Bge_Un_S;
                    break;
                case Code.Bgt_Un:
                    instruction.OpCode = OpCodes.Bgt_Un_S;
                    break;
                case Code.Ble_Un:
                    instruction.OpCode = OpCodes.Ble_Un_S;
                    break;
                case Code.Blt_Un:
                    instruction.OpCode = OpCodes.Blt_Un_S;
                    break;
                case Code.Leave:
                    instruction.OpCode = OpCodes.Leave_S;
                    break;
            }

            return true;
        }

        private static void ComputeOffsets(MethodBody body)
        {
            int offset = 0;
            foreach (Instruction instruction in body.Instructions)
            {
                instruction.Offset = offset;
                offset += instruction.GetSize();
            }
        }
    }
}