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

namespace mgaia.Utils
{
    /// <summary>
    /// Classe que gerencia agendamento de chamadas de métodos utilizando o game time da Unity
    /// </summary>
    /// <owner>Victor</owner>
    public static class GameTimeScheduler
    {
        private static GameTimeBridgeBehaviour sSchedulerBehaviour;
        private static List<SchedulePayload> sFixedUpdateSchedules = new List<SchedulePayload>();
        private static List<SchedulePayload> sUpdateSchedules = new List<SchedulePayload>();

        /// <summary>
        /// Tipo de schedule, em que timeline ele vai ser gerenciado
        /// </summary>
        public enum ScheduleType
        {
            Normal,
            FixedUpdate
        }

        /// <summary>
        /// Pausa ou despausa todos os schedules
        /// </summary>
        public static bool IsPaused
        {
            get { return !sSchedulerBehaviour.enabled; }
            set { sSchedulerBehaviour.enabled = !value; }
        }

        /// <summary>
        /// Cria o objeto centralizador de schedule, responsável por receber os eventos de timing da Unity
        /// </summary>
        public static void Create()
        {
            GameObject schedulerGameObject = new GameObject(Constants.SCHEDULER_OBJECT_NAME, typeof(GameTimeBridgeBehaviour));
            schedulerGameObject.isStatic = true;
            GameObject.DontDestroyOnLoad(schedulerGameObject);

            sSchedulerBehaviour = schedulerGameObject.GetComponent<GameTimeBridgeBehaviour>();
            sSchedulerBehaviour.Initialize(Update, FixedUpdate);
        }

		/// <summary>
		/// Testa se há um timer rodando com o delegate passado
		/// </summary>
		public static bool IsRunning(Action action)
		{
			SchedulePayload[] normalPayloads = GetPayload(ScheduleType.Normal).Where(payload => payload.Action.Equals(action)).ToArray();

			if (normalPayloads.Length > 0)
			{
				return true;
			}
			else
			{
                SchedulePayload[] fixedUpdatePayloads = GetPayload(ScheduleType.FixedUpdate).Where(payload => payload.Action.Equals(action)).ToArray();

				if (fixedUpdatePayloads.Length > 0)
				{
					return true;
				}
			}

			return false;
		}

        /// <summary>
        /// Seta o estado de pause da instância de action passada
        /// </summary>
        public static void SetPause(Action action, bool isPaused)
        {
            GetPayload(ScheduleType.Normal).Where(payload => payload.Action.Equals(action)).ForEach(payload => payload.Paused = isPaused);
            GetPayload(ScheduleType.FixedUpdate).Where(payload => payload.Action.Equals(action)).ForEach(payload => payload.Paused = isPaused);
        }

		/// <summary>
		/// Reseta os contadores de todas as instâncias da action passada
		/// </summary>
		public static void Reset(Action action)
		{
			GetPayload(ScheduleType.Normal).Where(payload => payload.Action.Equals(action)).ForEach(payload => payload.Reset());
            GetPayload(ScheduleType.FixedUpdate).Where(payload => payload.Action.Equals(action)).ForEach(payload => payload.Reset());
		}

        /// <summary>
        /// Cria um schedule
        /// </summary>
        public static void Schedule(ScheduleType scheduleType, Action action, float interval)
        {
            Schedule(scheduleType, action, Constants.INFINITY_NUMBER_OF_TIMES_TO_CALL, interval);
        }

        /// <summary>
        /// Cria um schedule
        /// </summary>
        public static void Schedule(ScheduleType scheduleType, Action action, uint numberOfTimesToCall, float interval)
        {
            Assert.AssertTrue(interval > 0, "Interval tem que ser maior que 0!");

            SchedulePayload payload = new SchedulePayload(action, numberOfTimesToCall, interval);

            GetPayload(scheduleType).Add(payload);
        }

        /// <summary>
        /// Remove todos os schedules utilizando a ação passada
        /// </summary>
        public static void RemoveAll(Action action)
        {
            MarkToDelete(ScheduleType.FixedUpdate, action);
            MarkToDelete(ScheduleType.Normal, action);
        }

        /// <summary>
        /// Atualiza os schedules que estão na timeline de update normal
        /// </summary>
        internal static void Update(float time)
        {
            RunUpdates(ScheduleType.Normal, time);
        }

        /// <summary>
        /// Atualiza os schedules que estão na timeline de fixed update
        /// </summary>
        /// <param name="time"></param>
        internal static void FixedUpdate(float time)
        {
            RunUpdates(ScheduleType.FixedUpdate, time);
        }

        /// <summary>
        /// Retorna a coleção de schedules que estão na timeline passada
        /// </summary>
        /// <param name="scheduleType"></param>
        /// <returns></returns>
        private static List<SchedulePayload> GetPayload(ScheduleType scheduleType)
        {
            List<SchedulePayload> schedulePayloadList = null;

            switch (scheduleType)
            {
                case ScheduleType.Normal:
                    schedulePayloadList = sUpdateSchedules;
                    break;
                case ScheduleType.FixedUpdate:
                    schedulePayloadList = sFixedUpdateSchedules;
                    break;
                default:
                    Assert.AssertTrue(false, "Tipo desconhecido de schedule!");
                    break;
            }

            return schedulePayloadList;
        }

        /// <summary>
        /// Marca pra remover um schedule na próxima atualização, evitando problemas de referência a objetos
        /// </summary>
        private static void MarkToDelete(ScheduleType scheduleType, Action action)
        {
            List<SchedulePayload> schedulePayloadList = GetPayload(scheduleType);

            IEnumerable<SchedulePayload> payloadsWithAction = schedulePayloadList.FindAll(lPayload => lPayload.Action.Equals(action));

            foreach (SchedulePayload payload in payloadsWithAction)
            {
                payload.NeedsToRemove = true;
            }
        }

        /// <summary>
        /// Atualiza todos os schedules na timeline passada e caso seja necessário chama eles
        /// </summary>
        private static void RunUpdates(ScheduleType scheduleType, float time)
        {
            List<SchedulePayload> schedulePayloadList = GetPayload(scheduleType);

            for (int idx = 0; idx < schedulePayloadList.Count; idx++)
            {
                SchedulePayload payload = schedulePayloadList[idx];

                if (payload.NeedsToRemove || !payload.Paused && (payload.NeedsToCall(time) && payload.Invoke()))
                {
                    schedulePayloadList.RemoveAt(idx);
                }
            }
        }
    }
}