using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;

using miew.Lambda;
using miew.String;

namespace miew.Reflection
{
	using String = System.String;

	static public class Extensions
	{

		public struct Retrolabel
		{
			public Label l;
			public int il_offset;
			public Retrolabel(ILGenerator il)
			{
				l = il.DefineLabel();
				il_offset = il.ILOffset;
				il.MarkLabel(l);
			}
		};

		public static void EmitLdcI4(this ILGenerator il, int i)
		{
			switch (i)
			{
				case -1:
					il.Emit(OpCodes.Ldc_I4_M1);
					break;
				case 0:
					il.Emit(OpCodes.Ldc_I4_0);
					break;
				case 1:
					il.Emit(OpCodes.Ldc_I4_1);
					break;
				case 2:
					il.Emit(OpCodes.Ldc_I4_2);
					break;
				case 3:
					il.Emit(OpCodes.Ldc_I4_3);
					break;
				case 4:
					il.Emit(OpCodes.Ldc_I4_4);
					break;
				case 5:
					il.Emit(OpCodes.Ldc_I4_5);
					break;
				case 6:
					il.Emit(OpCodes.Ldc_I4_6);
					break;
				case 7:
					il.Emit(OpCodes.Ldc_I4_7);
					break;
				case 8:
					il.Emit(OpCodes.Ldc_I4_8);
					break;
				default:
					{
						if (-128 <= i && i <= 127)
							il.Emit(OpCodes.Ldc_I4_S, (byte)i);
						else
							il.Emit(OpCodes.Ldc_I4, i);
					}
					break;
			}
		}

		public static Retrolabel MarkRetrolabel(this ILGenerator il)
		{
			return new Retrolabel(il);
		}

		public static void Emit(this ILGenerator il, OpCode op, Retrolabel retro)
		{
			if (il.ILOffset < retro.il_offset)
				throw new InvalidOperationException();
			bool f_s = (il.ILOffset + 7) - retro.il_offset <= 127;
			if (op == OpCodes.Br || op == OpCodes.Br_S)
				il.Emit(f_s ? OpCodes.Br_S : OpCodes.Br, retro.l);
			else if (op == OpCodes.Brtrue || op == OpCodes.Brtrue_S)
				il.Emit(f_s ? OpCodes.Brtrue_S : OpCodes.Brtrue, retro.l);
			else if (op == OpCodes.Brfalse || op == OpCodes.Brfalse_S)
				il.Emit(f_s ? OpCodes.Brfalse_S : OpCodes.Brfalse, retro.l);
			else
				throw new NotImplementedException();
		}

		public static IEnumerable<Object> PromoteAnonymous(this IEnumerable seq, String s_typename)
		{
			IEnumerator ie = seq.GetEnumerator();
			if (!ie.MoveNext())
				yield break;
			Object o = ie.Current;

			var e = AnonymousTypePromoter.GetPromotionInfo(s_typename, o, null, null, null);
			yield return e.Promote(o);

			while (ie.MoveNext())
				yield return e.Promote(ie.Current);
		}

		public static bool HasInterface(this Type t, Type T_int)
		{
			return t.GetInterface(T_int.Name) != null;
		}
	}

	public class AnonymousTypePromoter : CodeCompileUnit
	{
		public struct TypePromotionInfo
		{
			internal TypePromotionInfo(Assembly asm, Type type)
			{
				this.asm = asm;
				this.type = type;
			}

			readonly Assembly asm;
			readonly Type type;

			public Assembly Assembly { get { return asm; } }
			public Type Type { get { return type; } }

			public Object Promote(Object a)
			{
				return asm.CreateInstance(type.Name, false,
					BindingFlags.Public | BindingFlags.Instance, null, new Object[] { a }, null, null);
			}
			public IEnumerable<Object> Promote(IEnumerable seq)
			{
				foreach (Object o in seq)
					yield return asm.CreateInstance(type.Name, false,
						BindingFlags.Public | BindingFlags.Instance, null, new Object[] { o }, null, null);
			}
		};

		static Dictionary<Type, TypePromotionInfo> dict = new Dictionary<Type, TypePromotionInfo>();

		CodeTypeDeclaration target_class;
		String[] rgra = null;

		/// <summary>
		/// Create a class based on the fields in the specified prototypical instance of an anonymous type.
		/// The returned structure indicates the created type and assembly, and provides a method for converting
		/// other instances of the anonymous type to the newly created type. The specified name for the newly
		/// created class type is only used if this anonymous type has not been converted before.
		/// </summary>
		public static TypePromotionInfo GetPromotionInfo(
							String s_typename,
							Object prototype,
							String[] usings,
							String[] referenced_assemblies,
							Type[] base_types)
		{
			TypePromotionInfo e;
			Type T = prototype.GetType();
			if (!dict.TryGetValue(T, out e))
			{
				var atp = new AnonymousTypePromoter(s_typename, prototype, usings, referenced_assemblies, base_types);
#if false
				Debug.Print(atp.ToString());
#endif
				Assembly asm = atp.Compile();
				e = new TypePromotionInfo(asm, asm.GetType(s_typename, true, false));
				dict.Add(T, e);
			}
			return e;
		}

		AnonymousTypePromoter(String s_typename, Object prototype, String[] usings, String[] rgra, Type[] base_types)
		{
			this.rgra = rgra;
			CodeNamespace _namespace = new CodeNamespace();
			_namespace.Imports.Add(new CodeNamespaceImport("System"));
			if (usings != null)
				foreach (String u in usings)
					_namespace.Imports.Add(new CodeNamespaceImport(u));
			target_class = new CodeTypeDeclaration(s_typename);
			target_class.IsClass = true;
			target_class.TypeAttributes = TypeAttributes.Public;
			if (base_types != null)
				foreach (Type bt in base_types)
					target_class.BaseTypes.Add(new CodeTypeReference(bt));
			_namespace.Types.Add(target_class);
			this.Namespaces.Add(_namespace);

			AddConstructor(prototype);
		}

		void AddFieldAndAccessors(String s_field, String s_prop, Type t)
		{
			CodeMemberField fld = new CodeMemberField();
			fld.Attributes = MemberAttributes.Private;
			fld.Name = s_field;
			fld.Type = new CodeTypeReference(t);
			target_class.Members.Add(fld);

			CodeMemberProperty prop = new CodeMemberProperty();
			prop.Attributes = MemberAttributes.Public | MemberAttributes.Final;
			prop.Name = s_prop;
			prop.HasGet = true;
			prop.HasSet = true;
			prop.Type = new CodeTypeReference(t);
			prop.GetStatements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(
										new CodeThisReferenceExpression(), s_field)));
			prop.SetStatements.Add(new CodeAssignStatement(new CodeFieldReferenceExpression(
										new CodeThisReferenceExpression(), s_field),
										new CodeArgumentReferenceExpression("value")));
			target_class.Members.Add(prop);
		}

		void AddConstructor(Object prototype)
		{
			CodeConstructor constructor = new CodeConstructor();
			constructor.Attributes = MemberAttributes.Public | MemberAttributes.Final;
			target_class.Members.Add(constructor);
			constructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(Object), "obj"));
			CodeArgumentReferenceExpression o_arg = new CodeArgumentReferenceExpression("obj");

			constructor.Statements.Add(Anon<CodeVariableDeclarationStatement>.New(w =>
			{
				w.Type = new CodeTypeReference(typeof(FieldInfo[]));
				w.Name = "rgfi";
				w.InitExpression = Anon<CodeMethodInvokeExpression>.New(y =>
				{
					y.Method = new CodeMethodReferenceExpression(
											Anon<CodeMethodInvokeExpression>.New(x =>
											{
												x.Method = new CodeMethodReferenceExpression(o_arg, "GetType");
											}),
											"GetFields");
					y.Parameters.Add(new CodeBinaryOperatorExpression(
						new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(typeof(BindingFlags)), "NonPublic"),
						CodeBinaryOperatorType.BitwiseOr,
						new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(typeof(BindingFlags)), "Instance")));
				});
			}));

			CodeVariableReferenceExpression o_rgfi = new CodeVariableReferenceExpression("rgfi");

			int q = 0;
			foreach (var mi in prototype.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic))
			{
				String s_prop = mi.Name.ExtractAngleTagged();
				if (String.IsNullOrEmpty(s_prop))
					throw new Exception();
				String s_field = "__" + s_prop;
				Type T_prop = mi.FieldType;

				CodeFieldReferenceExpression fld_target = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), s_field);

				CodeCastExpression fld_source = new CodeCastExpression(new CodeTypeReference(T_prop),
					new CodeMethodInvokeExpression(Anon<CodeMethodReferenceExpression>.New(x =>
					{
						x.TargetObject = Anon<CodeArrayIndexerExpression>.New(v =>
						{
							v.Indices.Add(new CodePrimitiveExpression(q));
							v.TargetObject = o_rgfi;
						});
						x.MethodName = "GetValue";
					}),
						o_arg));

				constructor.Statements.Add(new CodeAssignStatement(fld_target, fld_source));

				AddFieldAndAccessors(s_field, s_prop, T_prop);
				q++;
			}
		}

		CodeDomProvider Provider
		{
			get { return CodeDomProvider.CreateProvider("csharp", new Dictionary<String, String> { { "CompilerVersion", "v4.0" } }); }
		}

		public override string ToString()
		{
			CodeGeneratorOptions opt = new CodeGeneratorOptions();
			opt.BracingStyle = "C";
			using (StringWriter sw = new StringWriter())
			{
				Provider.GenerateCodeFromCompileUnit(this, sw, opt);
				return sw.ToString();
			}
		}

		public Assembly Compile()
		{
			CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
			var cp = new CompilerParameters();
			cp.GenerateInMemory = true;
			if (rgra != null)
				foreach (String ra in rgra)
					cp.ReferencedAssemblies.Add(ra);
			var cr = Provider.CompileAssemblyFromDom(cp, new CodeCompileUnit[] { this });
#if false
			if (cr.Errors.Count > 0)
				Debugger.Break();
#endif
			return cr.CompiledAssembly;
		}
	};
}