using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Hooking;
using HSUI.Config;
using HSUI.Helpers;
using HSUI.Interface.Party;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Numerics;
using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler;

namespace HSUI.Interface.PartyCooldowns
{
    public unsafe class PartyCooldownsManager
    {
        #region Singleton
        public static PartyCooldownsManager Instance { get; private set; } = null!;
        private PartyCooldownsConfig _config = null!;
        private PartyCooldownsDataConfig _dataConfig = null!;

        private PartyCooldownsManager()
        {
            try
            {
                _onActionUsedHook = Plugin.GameInteropProvider.HookFromAddress<ActionEffectHandler.Delegates.Receive>(
                    ActionEffectHandler.MemberFunctionPointers.Receive,
                    OnActionUsed
                );
                _onActionUsedHook?.Enable();
            }
            catch
            {
                Plugin.Logger.Error("PartyCooldowns OnActionUsed Hook failed!!!");
            }

            try
            {
                _actorControlHook = Plugin.GameInteropProvider.HookFromSignature<ActorControlDelegate>(
                    "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64",
                    OnActorControl
                );
                _actorControlHook?.Enable();
            }
            catch
            {
                Plugin.Logger.Error("PartyCooldowns OnActorControl Hook failed!!!");
            }

            PartyManager.Instance.MembersChangedEvent += OnMembersChanged;
            ConfigurationManager.Instance.ResetEvent += OnConfigReset;
            Plugin.JobChangedEvent += OnJobChanged;
            Plugin.ClientState.TerritoryChanged += OnTerritoryChanged;

            ConfigReset(ConfigurationManager.Instance, false);
            UpdatePreview();
        }

        public static void Initialize()
        {
            Instance = new PartyCooldownsManager();
        }

        ~PartyCooldownsManager()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected void Dispose(bool disposing)
        {
            if (!disposing)
            {
                return;
            }

            _onActionUsedHook?.Disable();
            _onActionUsedHook?.Dispose();

            _actorControlHook?.Disable();
            _actorControlHook?.Dispose();

            PartyManager.Instance.MembersChangedEvent -= OnMembersChanged;
            Plugin.JobChangedEvent -= OnJobChanged;
            _config.ValueChangeEvent -= OnConfigPropertyChanged;
            _dataConfig.CooldownsDataEnabledChangedEvent -= OnCooldownEnabledChanged;
            Plugin.ClientState.TerritoryChanged -= OnTerritoryChanged;

            Instance = null!;
        }

        private void OnConfigReset(ConfigurationManager sender)
        {
            ConfigReset(sender);
        }

        private void ConfigReset(ConfigurationManager sender, bool forceUpdate = true)
        {
            if (_config != null)
            {
                _config.ValueChangeEvent -= OnConfigPropertyChanged;
            }

            _config = sender.GetConfigObject<PartyCooldownsConfig>();
            _config.ValueChangeEvent += OnConfigPropertyChanged;

            if (_dataConfig != null)
            {
                _dataConfig.CooldownsDataEnabledChangedEvent -= OnCooldownEnabledChanged;
            }

            _dataConfig = sender.GetConfigObject<PartyCooldownsDataConfig>();
            _dataConfig.CooldownsDataEnabledChangedEvent += OnCooldownEnabledChanged;
            _dataConfig.UpdateDataIfNeeded();

            if (forceUpdate)
            {
                ForcedUpdate();
            }
        }

        #endregion Singleton

        private Hook<ActionEffectHandler.Delegates.Receive>? _onActionUsedHook;

        private delegate void ActorControlDelegate(uint entityId, uint type, uint buffID, uint direct, uint actionId, uint sourceId, uint arg7, uint arg8, uint arg9, uint arg10, ulong targetId, byte arg12);
        private Hook<ActorControlDelegate>? _actorControlHook;

        private Dictionary<uint, Dictionary<uint, PartyCooldown>>? _oldMap;
        private Dictionary<uint, Dictionary<uint, PartyCooldown>> _cooldownsMap = new Dictionary<uint, Dictionary<uint, PartyCooldown>>();
        public IReadOnlyDictionary<uint, Dictionary<uint, PartyCooldown>> CooldownsMap => _cooldownsMap;

        private Dictionary<uint, double> _technicalStepMap = new Dictionary<uint, double>();

        public delegate void PartyCooldownsChangedEventHandler(PartyCooldownsManager sender);
        public event PartyCooldownsChangedEventHandler? CooldownsChangedEvent;

        private bool _wasInDuty = false;

        private void OnActorControl(uint entityId, uint type, uint buffID, uint direct, uint actionId, uint sourceId, uint arg7, uint arg8, uint arg9, uint arg10, ulong targetId, byte arg12)
        {
            _actorControlHook?.Original(entityId, type, buffID, direct, actionId, sourceId, arg7, arg8, arg9, arg10, targetId, arg12);

            // detect wipe fadeouts (not 100% reliable but good enough)
            if (type == 0x4000000F)
            {
                ResetCooldowns();
            }
        }

        private void ResetCooldowns()
        {
            foreach (uint actorId in _cooldownsMap.Keys)
            {
                foreach (PartyCooldown cooldown in _cooldownsMap[actorId].Values)
                {
                    cooldown.LastTimeUsed = 0;
                }
            }
        }

        private unsafe void OnActionUsed(uint actorId, Character* casterPtr, Vector3* targetPos, Header* header, TargetEffects* effects, GameObjectId* targetEntityIds)
        {
            _onActionUsedHook?.Original(actorId, casterPtr, targetPos, header, effects, targetEntityIds);

            // check if its an action
            if ((ActionType)header->ActionType != ActionType.Action ) { return; }

            // check if its a member in the party
            if (!_cooldownsMap.ContainsKey(actorId))
            {
                // check if its a party member's pet
                IGameObject? actor = Plugin.ObjectTable.SearchById(actorId);

                if (actor is IBattleNpc battleNpc && _cooldownsMap.ContainsKey(battleNpc.OwnerId))
                {
                    actorId = battleNpc.OwnerId;
                }
                else
                {
                    actorId = 0;
                }
            }

            if (actorId <= 0) { return; }

            uint actionID = header->ActionId;

            // special case for starry muse > set id to scenic muse
            if (actionID == 34675)
            {
                actionID = 35349;
            }

            // special case for technical step / finish
            // we detect when technical step is pressed and save the time
            // so we can properly calculate the cooldown once finish is pressed
            if (actionID == 16193 || actionID == 16194 || actionID == 16195 || actionID == 16196)
            {
                actionID = 16004;
            }

            if (actionID == 15998)
            {
                _technicalStepMap[actorId] = ImGui.GetTime();
            }
            else
            {
                // check if its an action we track
                if (_cooldownsMap[actorId].TryGetValue(actionID, out PartyCooldown? cooldown) && cooldown != null)
                {
                    // if its technical finish, we set the cooldown start time to
                    // the time when step was pressed
                    if (_technicalStepMap.TryGetValue(actorId, out double stepStartTime) && actionID == 16004)
                    {
                        cooldown.OverridenCooldownStartTime = stepStartTime;
                        _technicalStepMap.Remove(actorId);
                    }

                    double now = ImGui.GetTime();
                    cooldown.LastTimeUsed = now;
                    cooldown.IgnoreNextUse = false;

                    foreach (uint id in cooldown.Data.SharedActionIds)
                    {
                        if (_cooldownsMap[actorId].TryGetValue(id, out PartyCooldown? sharedCooldown) && sharedCooldown != null)
                        {
                            sharedCooldown.LastTimeUsed = now;
                            sharedCooldown.IgnoreNextUse = true;
                        }
                    }
                }
            }
        }

        public void ForcedUpdate()
        {
            OnMembersChanged(PartyManager.Instance);
        }

        private void OnMembersChanged(PartyManager sender)
        {
            Plugin.Framework.RunOnFrameworkThread(() =>
            {
                if (sender.Previewing || _config.Preview) { return; }

                _cooldownsMap.Clear();

                if (_config.ShowOnlyInDuties && !Plugin.Condition[ConditionFlag.BoundByDuty])
                {
                    CooldownsChangedEvent?.Invoke(this);
                    return;
                }

                // show when solo
                if (sender.IsSoloParty() || sender.MemberCount == 0)
                {
                    var player = Plugin.ObjectTable.LocalPlayer;
                    if (_config.ShowWhenSolo && player != null)
                    {
                        _cooldownsMap.Add((uint)player.GameObjectId, CooldownsForMember((uint)player.GameObjectId, player.ClassJob.RowId, player.Level, null));
                    }
                }
                else if (!_config.ShowOnlyInDuties || Plugin.Condition[ConditionFlag.BoundByDuty])
                {
                    // add new members
                    foreach (IPartyFramesMember member in sender.GroupMembers)
                    {
                        if (member.ObjectId > 0)
                        {
                            _cooldownsMap.Add(member.ObjectId, CooldownsForMember(member));
                        }
                    }
                }

                CooldownsChangedEvent?.Invoke(this);
            });
        }

        private Dictionary<uint, PartyCooldown> CooldownsForMember(IPartyFramesMember member)
        {
            return CooldownsForMember(member.ObjectId, member.JobId, member.Level, member);
        }

        private Dictionary<uint, PartyCooldown> CooldownsForMember(uint objectId, uint jobId, uint level, IPartyFramesMember? member)
        {
            Dictionary<uint, PartyCooldown> cooldowns = new Dictionary<uint, PartyCooldown>();

            foreach (PartyCooldownData data in _dataConfig.Cooldowns)
            {
                if (data.EnabledV2 != PartyCooldownEnabled.Disabled &&
                    level >= data.RequiredLevel &&
                    (data.DisabledAfterLevel == 0 || level < data.DisabledAfterLevel) &&
                    data.IsUsableBy(jobId) &&
                    !data.ExcludedJobIds.Contains(jobId))
                {
                    cooldowns.Add(data.ActionId, new PartyCooldown(data, objectId, level, member));
                }
            }

            return cooldowns;
        }

        #region events
        private void OnConfigPropertyChanged(object sender, OnChangeBaseArgs args)
        {
            if (args.PropertyName == "Preview")
            {
                UpdatePreview();
            }
            else if (args.PropertyName == "ShowWhenSolo" && PartyManager.Instance?.MemberCount == 0)
            {
                OnMembersChanged(PartyManager.Instance);
            }
            else if (args.PropertyName == "ShowOnlyInDuties" && PartyManager.Instance != null)
            {
                OnMembersChanged(PartyManager.Instance);
            }
        }

        private void OnJobChanged(uint jobId)
        {
            ForcedUpdate();
        }

        private void OnCooldownEnabledChanged(PartyCooldownsDataConfig config)
        {
            ForcedUpdate();
        }

        private void OnTerritoryChanged(ushort territoryId)
        {
            bool isInDuty = Plugin.Condition[ConditionFlag.BoundByDuty];
            if (_config.ShowOnlyInDuties && _wasInDuty != isInDuty)
            {
                ForcedUpdate();
            }

            _wasInDuty = isInDuty;
        }

        public void UpdatePreview()
        {
            if (!_config.Preview)
            {
                if (_oldMap != null)
                {
                    _cooldownsMap = _oldMap;
                }
                else
                {
                    _cooldownsMap.Clear();
                }

                if (PartyManager.Instance.Previewing)
                {
                    CooldownsChangedEvent?.Invoke(this);
                }
                else
                {
                    OnMembersChanged(PartyManager.Instance);
                }
                return;
            }

            if (PartyManager.Instance?.Previewing == false)
            {
                _oldMap = _cooldownsMap;
            }

            _cooldownsMap.Clear();
            Random RNG = new Random((int)ImGui.GetTime());

            for (uint i = 1; i < 9; i++)
            {
                Dictionary<uint, PartyCooldown> cooldowns = new Dictionary<uint, PartyCooldown>();

                JobRoles role = i < 3 ? JobRoles.Tank : (i < 5 ? JobRoles.Healer : JobRoles.Unknown);
                role = role == JobRoles.Unknown ? JobRoles.DPSMelee + RNG.Next(3) : role;
                int jobCount = JobsHelper.JobsByRole[role].Count;
                int jobIndex = RNG.Next(jobCount);
                uint jobId = JobsHelper.JobsByRole[role][jobIndex];

                _cooldownsMap.Add(i, CooldownsForMember(i, jobId, 90, null));

                foreach (PartyCooldown cooldown in _cooldownsMap[i].Values)
                {
                    int rng = RNG.Next(100);
                    if (rng > 80)
                    {
                        cooldown.LastTimeUsed = ImGui.GetTime() - 30;
                    }
                    else if (rng > 50)
                    {
                        cooldown.LastTimeUsed = ImGui.GetTime() + 1;
                    }
                }
            }

            CooldownsChangedEvent?.Invoke(this);
        }
        #endregion
    }
}
