//
// 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 System;
using System.Text;

namespace Mono.Cecil.Cil {

	public sealed class Instruction {

		internal int offset;
		internal OpCode opcode;
		internal object operand;

		internal Instruction previous;
		internal Instruction next;

		public int Offset {
			get { return offset; }
			set { offset = value; }
		}

		public OpCode OpCode {
			get { return opcode; }
			set { opcode = value; }
		}

		public object Operand {
			get { return operand; }
			set { operand = value; }
		}

		public Instruction Previous {
			get { return previous; }
			set { previous = value; }
		}

		public Instruction Next {
			get { return next; }
			set { next = value; }
		}

		internal Instruction (int offset, OpCode opCode)
		{
			this.offset = offset;
			this.opcode = opCode;
		}

		internal Instruction (OpCode opcode, object operand)
		{
			this.opcode = opcode;
			this.operand = operand;
		}

		public int GetSize ()
		{
			int size = opcode.Size;

			switch (opcode.OperandType) {
			case OperandType.InlineSwitch:
				return size + (1 + ((Instruction []) operand).Length) * 4;
			case OperandType.InlineI8:
			case OperandType.InlineR:
				return size + 8;
			case OperandType.InlineBrTarget:
			case OperandType.InlineField:
			case OperandType.InlineI:
			case OperandType.InlineMethod:
			case OperandType.InlineString:
			case OperandType.InlineTok:
			case OperandType.InlineType:
			case OperandType.ShortInlineR:
			case OperandType.InlineSig:
				return size + 4;
			case OperandType.InlineArg:
			case OperandType.InlineVar:
				return size + 2;
			case OperandType.ShortInlineBrTarget:
			case OperandType.ShortInlineI:
			case OperandType.ShortInlineArg:
			case OperandType.ShortInlineVar:
				return size + 1;
			default:
				return size;
			}
		}

		public override string ToString ()
		{
			var instruction = new StringBuilder ();

			AppendLabel (instruction, this);
			instruction.Append (':');
			instruction.Append (' ');
			instruction.Append (opcode.Name);

			if (operand == null)
				return instruction.ToString ();

			instruction.Append (' ');

			switch (opcode.OperandType) {
			case OperandType.ShortInlineBrTarget:
			case OperandType.InlineBrTarget:
				AppendLabel (instruction, (Instruction) operand);
				break;
			case OperandType.InlineSwitch:
				var labels = (Instruction []) operand;
				for (int i = 0; i < labels.Length; i++) {
					if (i > 0)
						instruction.Append (',');

					AppendLabel (instruction, labels [i]);
				}
				break;
			case OperandType.InlineString:
				instruction.Append ('\"');
				instruction.Append (operand);
				instruction.Append ('\"');
				break;
			default:
				instruction.Append (operand);
				break;
			}

			return instruction.ToString ();
		}

		static void AppendLabel (StringBuilder builder, Instruction instruction)
		{
			builder.Append ("IL_");
			builder.Append (instruction.offset.ToString ("x4"));
		}

		public static Instruction Create (OpCode opcode)
		{
			if (opcode.OperandType != OperandType.InlineNone)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, null);
		}

		public static Instruction Create (OpCode opcode, TypeReference type)
		{
			if (type == null)
				throw new ArgumentNullException ("type");
			if (opcode.OperandType != OperandType.InlineType &&
				opcode.OperandType != OperandType.InlineTok)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, type);
		}

		public static Instruction Create (OpCode opcode, CallSite site)
		{
			if (site == null)
				throw new ArgumentNullException ("site");
			if (opcode.Code != Code.Calli)
				throw new ArgumentException ("code");

			return new Instruction (opcode, site);
		}

		public static Instruction Create (OpCode opcode, MethodReference method)
		{
			if (method == null)
				throw new ArgumentNullException ("method");
			if (opcode.OperandType != OperandType.InlineMethod &&
				opcode.OperandType != OperandType.InlineTok)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, method);
		}

		public static Instruction Create (OpCode opcode, FieldReference field)
		{
			if (field == null)
				throw new ArgumentNullException ("field");
			if (opcode.OperandType != OperandType.InlineField &&
				opcode.OperandType != OperandType.InlineTok)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, field);
		}

		public static Instruction Create (OpCode opcode, string value)
		{
			if (value == null)
				throw new ArgumentNullException ("value");
			if (opcode.OperandType != OperandType.InlineString)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, value);
		}

		public static Instruction Create (OpCode opcode, sbyte value)
		{
			if (opcode.OperandType != OperandType.ShortInlineI &&
				opcode != OpCodes.Ldc_I4_S)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, value);
		}

		public static Instruction Create (OpCode opcode, byte value)
		{
			if (opcode.OperandType != OperandType.ShortInlineI ||
				opcode == OpCodes.Ldc_I4_S)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, value);
		}

		public static Instruction Create (OpCode opcode, int value)
		{
			if (opcode.OperandType != OperandType.InlineI)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, value);
		}

		public static Instruction Create (OpCode opcode, long value)
		{
			if (opcode.OperandType != OperandType.InlineI8)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, value);
		}

		public static Instruction Create (OpCode opcode, float value)
		{
			if (opcode.OperandType != OperandType.ShortInlineR)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, value);
		}

		public static Instruction Create (OpCode opcode, double value)
		{
			if (opcode.OperandType != OperandType.InlineR)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, value);
		}

		public static Instruction Create (OpCode opcode, Instruction target)
		{
			if (target == null)
				throw new ArgumentNullException ("target");
			if (opcode.OperandType != OperandType.InlineBrTarget &&
				opcode.OperandType != OperandType.ShortInlineBrTarget)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, target);
		}

		public static Instruction Create (OpCode opcode, Instruction [] targets)
		{
			if (targets == null)
				throw new ArgumentNullException ("targets");
			if (opcode.OperandType != OperandType.InlineSwitch)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, targets);
		}

		public static Instruction Create (OpCode opcode, VariableDefinition variable)
		{
			if (variable == null)
				throw new ArgumentNullException ("variable");
			if (opcode.OperandType != OperandType.ShortInlineVar &&
				opcode.OperandType != OperandType.InlineVar)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, variable);
		}

		public static Instruction Create (OpCode opcode, ParameterDefinition parameter)
		{
			if (parameter == null)
				throw new ArgumentNullException ("parameter");
			if (opcode.OperandType != OperandType.ShortInlineArg &&
				opcode.OperandType != OperandType.InlineArg)
				throw new ArgumentException ("opcode");

			return new Instruction (opcode, parameter);
		}
	}
}