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

// Invoke various actions after a given delay in time or frames.
// Scheduled actions are cancelled automatically when scenes change,
// although this can be overridden with the appropriate overloads.
// 
// Usage:
//   Delays.Invoke(myAction, 0.5f);           // Trigger myAction in half a second's time
//   Delays.Invoke(myAction, 30, true);       // Trigger myAction in 30 frames' time
//   Delays.SendEvent("myEvent"), 0.5f);      // Send "myEvent" in half a second's time
//   Delays.SendEvent("myEvent"), 30, true);  // Send "myEvent" in 30 frames' time
//
// Can also use up to two parameters in the action:
//   Delays.Invoke(myAction, param1, param2, 0.5f);      // Trigger myAction(param1, param2) in half a second's time. You will need to receive the parameters as "object" type and cast to something appropriate
//   Delays.Invoke(myAction, param1, param2, 30, true);  // Trigger myAction(param1, param2) in 30 frames' time. You will need to receive the parameters as "object" type and cast to something appropriate
//
namespace com.deeperbeige.lib
{
    public static class Delays
    {
		// Stores individual callback info
		class DelayedCallback
		{
			public Action                 action0             = null;
			public Action<object>         action1             = null;
			public Action<object, object> action2             = null;
			public object                 param1              = null;
			public object                 param2              = null;
			public string                 eventType           = string.Empty;
			public int                    onFrame             = int.MaxValue;
			public float                  afterTime           = float.MaxValue;
			public bool                   cancelOnSceneChange = true;
		}

		// Variables
		private static List<DelayedCallback> callbacks;  // Scheduled callbacks yet to fire (list is trimmed as each one fires)
		private static int                   frame;      // Class-wide concept of current frame, updated each frame. Should help avoid race conditions with setting up multiple invokes one line after another
		private static float                 time;       // Class-wide concept of time, updated each frame. Should help avoid race conditions with setting up multiple invokes one line after another
		private static bool                  initialised = false;
		private static void                  Init()
		{
			initialised = true;
			callbacks   = new List<DelayedCallback>();
			time        = Time.time;
			frame       = 0;

			Events.Listen(Events.EVENT_UPDATE,        DelaysOnUpdate,       false);
			Events.Listen(Events.EVENT_SCENE_CHANGED, DelaysOnSceneChanged, false);
		}
		
		// Scene changed clean-up
		private static void DelaysOnSceneChanged()
		{
			for (int j=0; j < callbacks.Count; j++)
			{
				if (! callbacks[j].cancelOnSceneChange) continue;
				
				callbacks.RemoveAt(j);
				j--;
			}
		}

		// Enter-frame
		private static void DelaysOnUpdate()
		{
			// Increment frame and time. This is carefully arranged here
			// such that callbacks that trigger more frame-delay callbacks
			// still work properly, and don't interfere with processing
			// existing callbacks and loop forever.
			frame++;
			time = Time.time;

			// Find all callbacks ready to fire this frame
			List<DelayedCallback> firing = new List<DelayedCallback>();
			for (int j = 0; j < callbacks.Count; j++)
			{
				DelayedCallback callback = callbacks[j];
				if (time  >= callback.afterTime ||
					frame >= callback.onFrame)
				{
					firing.Add(callback);
					callbacks.RemoveAt(j);
					j--;
				}
			}

			// Fire events ready to happen (these are safe to trigger more events etc)
			foreach (DelayedCallback callback in firing)
			{
				if (callback.action0 != null) callback.action0();
				if (callback.action1 != null) callback.action1(callback.param1);
				if (callback.action2 != null) callback.action2(callback.param1, callback.param2);
				if (! string.IsNullOrEmpty(callback.eventType)) Events.Send(callback.eventType);
			}
		}

		// Invoke an action after a specified period (seconds by default, frames if useFrames==true)
		public static void Invoke(Action callback, float delay) { Invoke(callback, delay, false); }
		public static void Invoke(Action callback, float delay, bool useFrames) { Invoke (callback, delay, useFrames, true); }
		public static void Invoke(Action callback, float delay, bool useFrames, bool cancelOnSceneChange)
		{
			DelayedCallback delayedCallback = new DelayedCallback();
			delayedCallback.action0 = callback;
			Schedule(delayedCallback, delay, useFrames, cancelOnSceneChange);
		}

		// Single parameter version
		public static void Invoke(Action<object> callback, object param1, float delay) { Invoke(callback, param1, delay, false); }
		public static void Invoke(Action<object> callback, object param1, float delay, bool useFrames) { Invoke(callback, param1, delay, useFrames, true); }
		public static void Invoke(Action<object> callback, object param1, float delay, bool useFrames, bool cancelOnSceneChange)
		{
			DelayedCallback delayedCallback = new DelayedCallback();
			delayedCallback.action1         = callback;
			delayedCallback.param1          = param1;
			Schedule(delayedCallback, delay, useFrames, cancelOnSceneChange);
		}

		// Double parameter version
		public static void Invoke(Action<object,object> callback, object param1, object param2, float delay) { Invoke(callback, param1, param2, delay, false); }
		public static void Invoke(Action<object,object> callback, object param1, object param2, float delay, bool useFrames) { Invoke(callback, param1, param2, delay, useFrames, true); }
		public static void Invoke(Action<object,object> callback, object param1, object param2, float delay, bool useFrames, bool cancelOnSceneChange)
		{
			DelayedCallback delayedCallback = new DelayedCallback();
			delayedCallback.action2         = callback;
			delayedCallback.param1          = param1;
			delayedCallback.param2          = param2;
			Schedule(delayedCallback, delay, useFrames, cancelOnSceneChange);
		}

		// Fire an event via the deeperbeige Events system after a specified period (seconds by default, frames if useFrames==true)
		public static void SendEvent(string eventType, float delay) { SendEvent(eventType, delay, false); }
		public static void SendEvent(string eventType, float delay, bool useFrames) { SendEvent(eventType, delay, false, true); }
		public static void SendEvent(string eventType, float delay, bool useFrames, bool cancelOnSceneChange)
		{
			DelayedCallback delayedCallback = new DelayedCallback();
			delayedCallback.eventType = eventType;
			Schedule(delayedCallback, delay, useFrames, cancelOnSceneChange);
		}

		// Schedule a callback to happen (internal usage)
		private static void Schedule(DelayedCallback delayedCallback, float delay, bool useFrames, bool cancelOnSceneChange)
		{
			if (! initialised) Init();
			
			if (useFrames)
			{
				delayedCallback.onFrame   = frame + (int)delay;
			} else {
				delayedCallback.afterTime = time  + delay;
			}
			
			delayedCallback.cancelOnSceneChange = cancelOnSceneChange;
			
			callbacks.Add(delayedCallback);
		}
	}
}
