/*
Copyright(c) 2021 xorus (https://github.com/xorus/EngageTimer)
Modifications Copyright(c) 2021 HSUI
09/21/2021 - Extracted code to hook the game's pulltimer functions.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Hooking;
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;

namespace HSUI.Helpers
{
    public unsafe class PullTimerHelper
    {
        #region Singleton
        private PullTimerHelper()
        {
            PullTimerState = new PullTimerState();

            try
            {
                _countdownTimerHook = Plugin.GameInteropProvider.HookFromAddress<AgentInterface.Delegates.Update>(
                    AgentModule.Instance()->GetAgentByInternalId(AgentId.CountDownSettingDialog)->VirtualTable->Update,
                    CountdownTimerFunc);
                _countdownTimerHook?.Enable();
            }
            catch
            {
                Plugin.Logger.Error("PullTimeHelper CountdownTimer Hook failed!!!");
            }
        }
        public static void Initialize() { Instance = new PullTimerHelper(); }
        public static PullTimerHelper Instance { get; private set; } = null!;

        ~PullTimerHelper()
        {
            Dispose(false);
        }

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

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

            _countdownTimerHook?.Disable();
            _countdownTimerHook?.Dispose();

            Instance = null!;
        }
        #endregion

        private DateTime _combatTimeEnd;
        private DateTime _combatTimeStart;

        private ulong _agentData;
        public bool CountDownRunning;

        private int _countDownStallTicks;

        private readonly Hook<AgentInterface.Delegates.Update>? _countdownTimerHook;
        public float LastCountDownValue;
        private bool _shouldRestartCombatTimer = true;
        private bool _lastMaxValueSet = false;

        public readonly PullTimerState PullTimerState;

        public void Update()
        {
            if (PullTimerState.Mocked)
            {
                return;
            }

            UpdateCountDown();
            UpdateEncounterTimer();
            PullTimerState.InInstance = Plugin.Condition[ConditionFlag.BoundByDuty];
        }

        private void CountdownTimerFunc(AgentInterface* agentInterface, uint frameCount)
        {
            _agentData = (ulong)agentInterface;
            _countdownTimerHook?.Original(agentInterface, frameCount);
        }

        private void UpdateEncounterTimer()
        {
            if (Plugin.Condition[ConditionFlag.InCombat])
            {
                PullTimerState.InCombat = true;
                if (_shouldRestartCombatTimer)
                {
                    _shouldRestartCombatTimer = false;
                    _combatTimeStart = DateTime.Now;
                }

                _combatTimeEnd = DateTime.Now;
            }
            else
            {
                PullTimerState.InCombat = false;
                _shouldRestartCombatTimer = true;
            }

            PullTimerState.CombatStart = _combatTimeStart;
            PullTimerState.CombatDuration = _combatTimeEnd - _combatTimeStart;
            PullTimerState.CombatEnd = _combatTimeEnd;
        }

        private void UpdateCountDown()
        {
            PullTimerState.CountingDown = false;

            if (_agentData == 0)
            {
                return;
            }

            byte countdownActive = Marshal.PtrToStructure<byte>((IntPtr)_agentData + 0x38);
            if (countdownActive == 0)
            {
                _lastMaxValueSet = false;
                return;
            }

            float countDownPointerValue = Marshal.PtrToStructure<float>((IntPtr)_agentData + 0x2c);

            // is last value close enough (workaround for floating point approx)
            if (Math.Abs(countDownPointerValue - LastCountDownValue) < 0.001f)
            {
                _countDownStallTicks++;
            }
            else
            {
                _countDownStallTicks = 0;
                CountDownRunning = true;
            }

            if (_countDownStallTicks > 50)
            {
                CountDownRunning = false;
            }

            if (countDownPointerValue > 0 && CountDownRunning)
            {
                PullTimerState.CountDownValue = countDownPointerValue;
                PullTimerState.CountingDown = true;
            }

            if (!_lastMaxValueSet && CountDownRunning)
            {
                PullTimerState.CountDownMax = countDownPointerValue;
                _lastMaxValueSet = true;
            }
            else if (_lastMaxValueSet && countDownPointerValue <= 0)
            {
                _lastMaxValueSet = false;
            }

            LastCountDownValue = countDownPointerValue;
        }
    }

    public class PullTimerState
    {
        private bool _inCombat;
        private bool _countingDown;
        public TimeSpan CombatDuration { get; set; }
        public DateTime CombatEnd { get; set; }
        public DateTime CombatStart { get; set; }

        public bool Mocked { get; set; }

        public bool InCombat
        {
            get => _inCombat;
            set
            {
                if (_inCombat == value)
                {
                    return;
                }

                _inCombat = value;
                InCombatChanged?.Invoke(this, EventArgs.Empty);
            }
        }

        public bool CountingDown
        {
            get => _countingDown;
            set
            {
                if (_countingDown == value)
                {
                    return;
                }

                _countingDown = value;
                CountingDownChanged?.Invoke(this, EventArgs.Empty);
            }
        }

        public bool InInstance { get; set; }

        public float CountDownValue { get; set; } = 0f;
        public float CountDownMax { get; set; } = 0f;
        public event EventHandler? InCombatChanged;
        public event EventHandler? CountingDownChanged;
    }
}
