using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public static class AsyncManager
{
	private class Pair
	{
		public Coroutine Routine;
		public bool Running;
	}
		
	private static AsyncSurrogate sSurrogate = null;
	private static Dictionary<IEnumerator, Pair> sExecuting = new Dictionary<IEnumerator, Pair>();
	
	private static AsyncSurrogate Surrogate
	{
		get
		{
			if (sSurrogate == null)
			{
				sSurrogate = new GameObject("AsyncSurrogate").AddComponent<AsyncSurrogate>();
			}
			return sSurrogate;
		}
	}
	
	private static IEnumerator Start(IEnumerator coroutine)
	{
		// TODO: Fanta- Testar configuração onde a corotina inicia uma corotina aninhada. Ex: yield return Execute(...)
		if (coroutine == null)
		{
			throw new System.ArgumentNullException("coroutine");
		}
		
		Debug.Assert(!sExecuting.ContainsKey(coroutine), "Enumerator already present on executing dictionary");
		sExecuting[coroutine] = new Pair() { Routine = null, Running = true };
		
		while (coroutine.MoveNext())
		{
			IYieldInstruction current = coroutine.Current as IYieldInstruction;
			if (current != null)
			{ // É um IYieldInstruction
				while (!current.IsFinished)
				{
					yield return new WaitForEndOfFrame();
					
					if (!sExecuting[coroutine].Running)
					{ // Se flag foi desabilitada, encerra execução
						break;
					}
				}
			}
			else
			{
				yield return coroutine.Current;
			}
			
			if (!sExecuting[coroutine].Running)
			{ // Se flag foi desabilitada, encerra execução
				break;
			}
		}
		
		sExecuting.Remove(coroutine);
	}
	
	private static Coroutine Invoke(MonoBehaviour behaviour, IEnumerator coroutine)
	{
		// Caso o behaviour for vazio, inicia a corotina a partir do surrogate
		Coroutine routine = (behaviour ?? Surrogate).StartCoroutine(Start(coroutine));
		sExecuting[coroutine].Routine = routine;
		
		return routine;
	}
	
	private static IEnumerator ExecuteDelayed(float delay, System.Func<IEnumerator> function)
	{
		yield return new WaitForSeconds(delay);
		IEnumerator enumerator = function();
		while (enumerator.MoveNext())
		{
			yield return enumerator.Current;
		}
	}
	
	private static IEnumerator ExecuteDelayed(float delay, System.Action action)
	{
		yield return new WaitForSeconds(delay);
		action();
	}
	
	public static Coroutine Invoke(this MonoBehaviour behaviour, System.Func<IEnumerator> function)
	{
		if (function == null)
		{
			throw new System.ArgumentNullException("function");
		}
		return Invoke(behaviour, function());
	}
	
	public static Coroutine Execute(this MonoBehaviour behaviour, System.Func<IEnumerator> function)
	{
		if (function == null)
		{
			throw new System.ArgumentNullException("function");
		}
		return Invoke(behaviour, function());
	}
	
	public static Coroutine Execute<T>(this MonoBehaviour behaviour, System.Func<T, IEnumerator> function, T parameter)
	{
		if (function == null)
		{
			throw new System.ArgumentNullException("function");
		}
		return Invoke(behaviour, function(parameter));
	}
	
	public static Coroutine Execute<T1, T2>(this MonoBehaviour behaviour, System.Func<T1, T2, IEnumerator> function, T1 first, T2 second)
	{
		if (function == null)
		{
			throw new System.ArgumentNullException("function");
		}
		return Invoke(behaviour, function(first, second));
	}

    public static Coroutine Execute<T1, T2, T3, T4>(this MonoBehaviour behaviour, System.Func<T1, T2, T3, T4, IEnumerator> function, T1 first, T2 second, T3 third, T4 fourth)
    {
        if (function == null)
        {
            throw new System.ArgumentNullException("function");
        }
        return Invoke(behaviour, function(first, second, third, fourth));
    }
	
	public static Coroutine Execute(this MonoBehaviour behaviour, float delay, System.Func<IEnumerator> function)
	{
		if (function == null)
		{
			throw new System.ArgumentNullException("function");
		}
		if (delay < 0)
		{
			throw new System.ArgumentOutOfRangeException("delay", "delay must be non-negative");
		}
		return Execute(behaviour, ExecuteDelayed, delay, function);
	}
	
	public static Coroutine ExecuteAction(this MonoBehaviour behaviour, float delay, System.Action action)
	{
		if (action == null)
		{
			throw new System.ArgumentNullException("action");
		}
		if (delay < 0)
		{
			throw new System.ArgumentOutOfRangeException("delay", "delay must be non-negative");
		}
		return Execute(behaviour, ExecuteDelayed, delay, action);
	}
	
	public static void Stop(this Coroutine routine)
	{
		if (routine == null)
		{
			throw new System.ArgumentNullException("routine");
		}
		try
		{
			var running = sExecuting.Where((r) => r.Value.Routine.Equals(routine)); // Corotinas executando
			if (!running.IsEmpty())
			{
				running.Single().Value.Running = false;
			}
			
		}
		catch (System.InvalidOperationException)
		{
			throw new System.ArgumentException("Provided Coroutine not started with Execute", "routine");
		}
	}
	
	public static bool IsExecuting(this Coroutine routine)
	{
		if (routine == null)
		{
			throw new System.ArgumentNullException("routine");
		}
		return sExecuting.Any((r) => r.Value.Routine.Equals(routine));
	}
}