using System;
using System.Collections.Generic;
using System.Runtime.Remoting.Messaging;
using System.Threading;
using System.Threading.Tasks;

namespace Mono.Debugger.Soft
{
	public class InvokeResult {
		public Value Result { get; set; }
		//
		// The value of the receiver after the call for calls to valuetype methods or null.
		// Only set when using the InvokeOptions.ReturnOutThis flag.
		// Since protocol version 2.35
		//
		public Value OutThis { get; set; }
		//
		// The value of the arguments after the call
		// Only set when using the InvokeOptions.ReturnOutArgs flag.
		// Since protocol version 2.35
		//
		public Value[] OutArgs { get; set; }
	}

	public class ObjectMirror : Value {
		TypeMirror type;
		AppDomainMirror domain;
	
		internal ObjectMirror (VirtualMachine vm, long id) : base (vm, id) {
		}
	
		internal ObjectMirror (VirtualMachine vm, long id, TypeMirror type, AppDomainMirror domain) : base (vm, id) {
			this.type = type;
			this.domain = domain;
		}

		void GetInfo () {
			var info = vm.conn.Object_GetInfo (id);
			type = vm.GetType (info.type_id);
			domain = vm.GetDomain (info.domain_id);
		}

		public TypeMirror Type {
			get {
				if (type == null) {
					if (vm.conn.Version.AtLeast (2, 5))
						GetInfo ();
					else
				 		type = vm.GetType (vm.conn.Object_GetType (id));
				}
				return type;
			}
		}

		public AppDomainMirror Domain {
			get {
				if (domain == null) {
					if (vm.conn.Version.AtLeast (2, 5))
						GetInfo ();
					else
						domain = vm.GetDomain (vm.conn.Object_GetDomain (id));
				}
				return domain;
			}
		}

		public bool IsCollected {
			get {
				return vm.conn.Object_IsCollected (id);
			}
		}

		public Value GetValue (FieldInfoMirror field) {
			return GetValues (new FieldInfoMirror [] { field }) [0];
		}

		public Value[] GetValues (IList<FieldInfoMirror> fields) {
			if (fields == null)
				throw new ArgumentNullException ("fields");
			foreach (FieldInfoMirror f in fields) {
				if (f == null)
					throw new ArgumentNullException ("field");
				CheckMirror (f);
			}
			long[] ids = new long [fields.Count];
			for (int i = 0; i < fields.Count; ++i)
				ids [i] = fields [i].Id;
			try {
				return vm.DecodeValues (vm.conn.Object_GetValues (id, ids));
			} catch (CommandException ex) {
				if (ex.ErrorCode == ErrorCode.INVALID_FIELDID) {
					if (fields.Count == 1)
						throw new ArgumentException (string.Format ("The field '{0}' is not valid for this type.", fields[0].Name));
					throw new ArgumentException ("One of the fields is not valid for this type.", "fields");
				} else
					throw;
			}
		}

		public void SetValues (IList<FieldInfoMirror> fields, Value[] values) {
			if (fields == null)
				throw new ArgumentNullException ("fields");
			if (values == null)
				throw new ArgumentNullException ("values");
			foreach (FieldInfoMirror f in fields) {
				if (f == null)
					throw new ArgumentNullException ("field");
				CheckMirror (f);
			}
			foreach (Value v in values) {
				if (v == null)
					throw new ArgumentNullException ("values");
				CheckMirror (v);
			}
			long[] ids = new long [fields.Count];
			for (int i = 0; i < fields.Count; ++i)
				ids [i] = fields [i].Id;
			try {
				vm.conn.Object_SetValues (id, ids, vm.EncodeValues (values));
			} catch (CommandException ex) {
				if (ex.ErrorCode == ErrorCode.INVALID_FIELDID)
					throw new ArgumentException ("One of the fields is not valid for this type.", "fields");
				else if (ex.ErrorCode == ErrorCode.INVALID_ARGUMENT)
					throw new ArgumentException ("One of the values is not valid for its field.", "values");
				else
					throw;
			}
		}

		public void SetValue (FieldInfoMirror field, Value value) {
			SetValues (new FieldInfoMirror [] { field }, new Value [] { value });
		}

		/*
		 * The current address of the object. It can change during garbage 
		 * collections. Use a long since the debuggee might have a different 
		 * pointer size. 
		 */
		public long Address {
			get {
				return vm.conn.Object_GetAddress (id);
			}
		}

		public Value InvokeMethod (ThreadMirror thread, MethodMirror method, IList<Value> arguments) {
			return InvokeMethod (vm, thread, method, this, arguments, InvokeOptions.None);
		}

		public Value InvokeMethod (ThreadMirror thread, MethodMirror method, IList<Value> arguments, InvokeOptions options) {
			return InvokeMethod (vm, thread, method, this, arguments, options);
		}

		[Obsolete ("Use the overload without the 'vm' argument")]
		public IAsyncResult BeginInvokeMethod (VirtualMachine vm, ThreadMirror thread, MethodMirror method, IList<Value> arguments, InvokeOptions options, AsyncCallback callback, object state) {
			return BeginInvokeMethod (vm, thread, method, this, arguments, options, callback, state);
		}

		public IAsyncResult BeginInvokeMethod (ThreadMirror thread, MethodMirror method, IList<Value> arguments, InvokeOptions options, AsyncCallback callback, object state) {
			return BeginInvokeMethod (vm, thread, method, this, arguments, options, callback, state);
		}

		public Value EndInvokeMethod (IAsyncResult asyncResult) {
			return EndInvokeMethodInternal (asyncResult);
		}

		public InvokeResult EndInvokeMethodWithResult (IAsyncResult asyncResult) {
			return  ObjectMirror.EndInvokeMethodInternalWithResult (asyncResult);
		}

		public Task<Value> InvokeMethodAsync (ThreadMirror thread, MethodMirror method, IList<Value> arguments, InvokeOptions options = InvokeOptions.None) {
			var tcs = new TaskCompletionSource<Value> ();
			BeginInvokeMethod (thread, method, arguments, options, iar =>
					{
						try {
							tcs.SetResult (EndInvokeMethod (iar));
						} catch (OperationCanceledException) {
							tcs.TrySetCanceled ();
						} catch (Exception ex) {
							tcs.TrySetException (ex);
						}
					}, null);
			return tcs.Task;
		}

		public Task<InvokeResult> InvokeMethodAsyncWithResult (ThreadMirror thread, MethodMirror method, IList<Value> arguments, InvokeOptions options = InvokeOptions.None) {
			var tcs = new TaskCompletionSource<InvokeResult> ();
			BeginInvokeMethod (thread, method, arguments, options, iar =>
					{
						try {
							tcs.SetResult (EndInvokeMethodInternalWithResult (iar));
						} catch (OperationCanceledException) {
							tcs.TrySetCanceled ();
						} catch (Exception ex) {
							tcs.TrySetException (ex);
						}
					}, null);
			return tcs.Task;
		}

		//
		// Invoke the members of METHODS one-by-one, calling CALLBACK after each invoke was finished. The IAsyncResult will be marked as completed after all invokes have
		// finished. The callback will be called with a different IAsyncResult that represents one method invocation.
		// From protocol version 2.22.
		//
		public IAsyncResult BeginInvokeMultiple (ThreadMirror thread, MethodMirror[] methods, IList<IList<Value>> arguments, InvokeOptions options, AsyncCallback callback, object state) {
			return BeginInvokeMultiple (vm, thread, methods, this, arguments, options, callback, state);
		}

		public void EndInvokeMultiple (IAsyncResult asyncResult) {
			EndInvokeMultipleInternal (asyncResult);
		}

		/*
		 * Common implementation for invokes
		 */

		class InvokeAsyncResult : IInvokeAsyncResult {

			public object AsyncState {
				get; set;
			}

			public WaitHandle AsyncWaitHandle {
				get; set;
			}

			public bool CompletedSynchronously {
				get {
					return false;
				}
			}

			public bool IsCompleted {
				get; set;
			}

			public AsyncCallback Callback {
				get; set;
			}

			public ErrorCode ErrorCode {
				get; set;
			}

			public VirtualMachine VM {
				get; set;
			}

			public ThreadMirror Thread {
				get; set;
			}

			public ValueImpl Value {
				get; set;
			}

			public ValueImpl OutThis {
				get; set;
			}

			public ValueImpl[] OutArgs {
				get; set;
			}

			public ValueImpl Exception {
				get; set;
			}

			public int ID {
				get; set;
			}

			public bool IsMultiple {
				get; set;
			}
			   
			public int NumPending;

			public void Abort ()
			{
				if (ID == 0) // Ooops
					return;

				ObjectMirror.AbortInvoke (VM, Thread, ID);
			}
		}

		internal static IInvokeAsyncResult BeginInvokeMethod (VirtualMachine vm, ThreadMirror thread, MethodMirror method, Value this_obj, IList<Value> arguments, InvokeOptions options, AsyncCallback callback, object state) {
			if (thread == null)
				throw new ArgumentNullException ("thread");
			if (method == null)
				throw new ArgumentNullException ("method");
			if (arguments == null)
				arguments = new Value [0];

			InvokeFlags f = InvokeFlags.NONE;

			if ((options & InvokeOptions.DisableBreakpoints) != 0)
				f |= InvokeFlags.DISABLE_BREAKPOINTS;
			if ((options & InvokeOptions.SingleThreaded) != 0)
				f |= InvokeFlags.SINGLE_THREADED;
			if ((options & InvokeOptions.ReturnOutThis) != 0)
				f |= InvokeFlags.OUT_THIS;
			if ((options & InvokeOptions.ReturnOutArgs) != 0)
				f |= InvokeFlags.OUT_ARGS;
			if ((options & InvokeOptions.Virtual) != 0)
				f |= InvokeFlags.VIRTUAL;

			InvokeAsyncResult r = new InvokeAsyncResult { AsyncState = state, AsyncWaitHandle = new ManualResetEvent (false), VM = vm, Thread = thread, Callback = callback };
			thread.InvalidateFrames ();
			r.ID = vm.conn.VM_BeginInvokeMethod (thread.Id, method.Id, this_obj != null ? vm.EncodeValue (this_obj) : vm.EncodeValue (vm.CreateValue (null)), vm.EncodeValues (arguments), f, InvokeCB, r);

			return r;
		}

		// This is called when the result of an invoke is received
		static void InvokeCB (ValueImpl v, ValueImpl exc, ValueImpl out_this, ValueImpl[] out_args, ErrorCode error, object state) {
			InvokeAsyncResult r = (InvokeAsyncResult)state;

			if (error != 0) {
				r.ErrorCode = error;
			} else {
				r.Value = v;
				r.Exception = exc;
			}

			r.OutThis = out_this;
			r.OutArgs = out_args;

			r.IsCompleted = true;
			((ManualResetEvent)r.AsyncWaitHandle).Set ();

			if (r.Callback != null)
				r.Callback.BeginInvoke (r, null, null);
		}

	    internal static InvokeResult EndInvokeMethodInternalWithResult (IAsyncResult asyncResult) {
			if (asyncResult == null)
				throw new ArgumentNullException ("asyncResult");

			InvokeAsyncResult r = (InvokeAsyncResult)asyncResult;

			if (!r.IsCompleted)
				r.AsyncWaitHandle.WaitOne ();

			if (r.ErrorCode != 0) {
				try {
					r.VM.ErrorHandler (null, new ErrorHandlerEventArgs () { ErrorCode = r.ErrorCode });
				} catch (CommandException ex) {
					if (ex.ErrorCode == ErrorCode.INVALID_ARGUMENT)
						throw new ArgumentException ("Incorrect number or types of arguments", "arguments");

					throw;
				}
				throw new NotImplementedException ();
			} else {
				if (r.Exception != null)
					throw new InvocationException ((ObjectMirror)r.VM.DecodeValue (r.Exception));

				Value out_this = null;
				if (r.OutThis != null)
					out_this = r.VM.DecodeValue (r.OutThis);
				Value[] out_args = null;
				if (r.OutArgs != null)
					out_args = r.VM.DecodeValues (r.OutArgs);

				return new InvokeResult () { Result = r.VM.DecodeValue (r.Value), OutThis = out_this, OutArgs = out_args };
			}
		}

 	    internal static Value EndInvokeMethodInternal (IAsyncResult asyncResult) {
			InvokeResult res = EndInvokeMethodInternalWithResult (asyncResult);
			return res.Result;
		}

	    internal static void EndInvokeMultipleInternal (IAsyncResult asyncResult) {
			if (asyncResult == null)
				throw new ArgumentNullException ("asyncResult");

			InvokeAsyncResult r = (InvokeAsyncResult)asyncResult;

			if (!r.IsCompleted)
				r.AsyncWaitHandle.WaitOne ();
		}

		internal static Value InvokeMethod (VirtualMachine vm, ThreadMirror thread, MethodMirror method, Value this_obj, IList<Value> arguments, InvokeOptions options) {
			return EndInvokeMethodInternal (BeginInvokeMethod (vm, thread, method, this_obj, arguments, options, null, null));
		}

		internal static void AbortInvoke (VirtualMachine vm, ThreadMirror thread, int id)
		{
			vm.conn.VM_AbortInvoke (thread.Id, id);
		}

		//
		// Implementation of InvokeMultiple
		//

		internal static IInvokeAsyncResult BeginInvokeMultiple (VirtualMachine vm, ThreadMirror thread, MethodMirror[] methods, Value this_obj, IList<IList<Value>> arguments, InvokeOptions options, AsyncCallback callback, object state) {
			if (thread == null)
				throw new ArgumentNullException ("thread");
			if (methods == null)
				throw new ArgumentNullException ("methods");
			foreach (var m in methods)
				if (m == null)
					throw new ArgumentNullException ("method");
			if (arguments == null) {
				arguments = new List<IList<Value>> ();
				for (int i = 0; i < methods.Length; ++i)
					arguments.Add (new Value [0]);
			} else {
				// FIXME: Not needed for property evaluation
				throw new NotImplementedException ();
			}
			if (callback == null)
				throw new ArgumentException ("A callback argument is required for this method.", "callback");

			InvokeFlags f = InvokeFlags.NONE;

			if ((options & InvokeOptions.DisableBreakpoints) != 0)
				f |= InvokeFlags.DISABLE_BREAKPOINTS;
			if ((options & InvokeOptions.SingleThreaded) != 0)
				f |= InvokeFlags.SINGLE_THREADED;

			InvokeAsyncResult r = new InvokeAsyncResult { AsyncState = state, AsyncWaitHandle = new ManualResetEvent (false), VM = vm, Thread = thread, Callback = callback, NumPending = methods.Length, IsMultiple = true };

			var mids = new long [methods.Length];
			for (int i = 0; i < methods.Length; ++i)
				mids [i] = methods [i].Id;
			var args = new List<ValueImpl[]> ();
			for (int i = 0; i < methods.Length; ++i)
				args.Add (vm.EncodeValues (arguments [i]));
			thread.InvalidateFrames ();
			r.ID = vm.conn.VM_BeginInvokeMethods (thread.Id, mids, this_obj != null ? vm.EncodeValue (this_obj) : vm.EncodeValue (vm.CreateValue (null)), args, f, InvokeMultipleCB, r);

			return r;
		}

		// This is called when the result of an invoke is received
		static void InvokeMultipleCB (ValueImpl v, ValueImpl exc, ValueImpl out_this, ValueImpl[] out_args, ErrorCode error, object state) {
			var r = (InvokeAsyncResult)state;

			Interlocked.Decrement (ref r.NumPending);

			if (r.NumPending == 0) {
				r.IsCompleted = true;
				((ManualResetEvent)r.AsyncWaitHandle).Set ();
			}

			// Have to pass another asyncresult to the callback since multiple threads can execute it concurrently with results of multiple invocations
			var r2 = new InvokeAsyncResult { AsyncState = r.AsyncState, AsyncWaitHandle = null, VM = r.VM, Thread = r.Thread, Callback = r.Callback, IsCompleted = true };

			if (error != 0) {
				r2.ErrorCode = error;
			} else {
				r2.Value = v;
				r2.Exception = exc;
			}

			r.Callback.BeginInvoke (r2, null, null);
		}
	}
}
