using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace PkmnLibSharp.Battling.Events
{
    public class BattleEventListener
    {
        public delegate Task BattleEventDelegate(EventData evt);
        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate void BattleEventPtrDelegate(IntPtr ptr);

        private readonly BattleEventDelegate _del;
        // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
        private readonly BattleEventPtrDelegate _innerDel;
        internal readonly IntPtr FunctionPtr;
        private Task? _currentTask;

        public BattleEventListener(BattleEventDelegate del)
        {
            _del = del;
            _innerDel = OnEventPtr;
            FunctionPtr = Marshal.GetFunctionPointerForDelegate(_innerDel);
        }

        public void EnsureFinishedListening()
        {
            try
            {
                _currentTask?.Wait();
            }
            catch (AggregateException e)
            {
                LogHandler.Log(LogHandler.LogLevel.Error, e.GetBaseException().Message);
            }
            catch (Exception e)
            {
                LogHandler.Log(LogHandler.LogLevel.Error, e.Message);
            }
        }

        private void OnEventPtr(IntPtr ptr)
        {
            // If we're still registering another event, wait until that's done. This ensures events always arrive in
            // the correct order.
            EnsureFinishedListening();
            // We do however want to run the event handler async, so that the battle code can keep running up till its
            // next event.
            _currentTask = Task.Run(() => InternalRunner(ptr));
        }

        private async Task InternalRunner(IntPtr ptr)
        {
            var wrapped = WrapEventPtr(ptr);
            await _del.Invoke(wrapped);
        }
        
        private static EventData WrapEventPtr(IntPtr ptr)
        {
            var evtType = (EventDataKind)Creaturelib.Generated.EventData.GetKind(ptr);
            switch (evtType)
            {
                case EventDataKind.Damage:
                    return new DamageEvent(evtType, ptr);
                case EventDataKind.Heal:
                    return new HealEvent(evtType, ptr);
                case EventDataKind.Faint:
                    return new FaintEvent(evtType, ptr);
                case EventDataKind.Switch:
                    return new SwitchEvent(evtType, ptr);
                case EventDataKind.TurnStart:
                    return new TurnStartEvent(evtType, ptr);
                case EventDataKind.TurnEnd:
                    return new TurnEndEvent(evtType, ptr);
                case EventDataKind.ExperienceGain:
                    return new ExperienceGainEvent(evtType, ptr);
                case EventDataKind.DisplayText:
                    return new DisplayTextEvent(evtType, ptr);
                case EventDataKind.Miss:
                    return new MissEvent(evtType, ptr);
                case EventDataKind.ChangeSpecies:
                    return new ChangeSpeciesEvent(evtType, ptr);
                case EventDataKind.ChangeVariant:
                    return new ChangeFormeEvent(evtType, ptr);
                case EventDataKind.AttackUse:
                    return new MoveUseEvent(evtType, ptr);
                case EventDataKind.WeatherChange:
                    break;
                case EventDataKind.StatusChange:
                    break;
                default:
                    throw new ArgumentOutOfRangeException($"Unknown event kind: " + evtType);
            }
            throw new NotImplementedException($"Unhandled battle event: '{evtType}'");
        }
    }
}