using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.Interop;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Handlers;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using Map = Lumina.Excel.Sheets.Map;
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;

namespace LightlessSync.Services;

public class DalamudUtilService : IHostedService, IMediatorSubscriber
{
    private readonly List<uint> _classJobIdsIgnoredForPets = [30];
    private readonly IClientState _clientState;
    private readonly ICondition _condition;
    private readonly IDataManager _gameData;
    private readonly IGameConfig _gameConfig;
    private readonly IPlayerState _playerState;
    private readonly BlockedCharacterHandler _blockedCharacterHandler;
    private readonly IFramework _framework;
    private readonly IGameGui _gameGui;
    private readonly ILogger<DalamudUtilService> _logger;
    private readonly IObjectTable _objectTable;
    private readonly ActorObjectService _actorObjectService;
    private readonly ITargetManager _targetManager;
    private readonly PerformanceCollectorService _performanceCollector;
    private readonly LightlessConfigService _configService;
    private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
    private readonly Lazy<PairFactory> _pairFactory;
    private PairUniqueIdentifier? _FocusPairIdent;
    private IGameObject? _FocusOriginalTarget;
    private uint? _classJobId = 0;
    private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow;
    private string _lastGlobalBlockPlayer = string.Empty;
    private string _lastGlobalBlockReason = string.Empty;
    private ushort _lastZone = 0;
    private ushort _lastWorldId = 0;
    private uint _lastMapId = 0;
    private bool _sentBetweenAreas = false;
    private Lazy<ulong> _cid;

    public DalamudUtilService(ILogger<DalamudUtilService> logger, IClientState clientState, IObjectTable objectTable, IFramework framework,
        IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig, IPlayerState playerState,
        ActorObjectService actorObjectService, BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector,
        LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService, Lazy<PairFactory> pairFactory)
    {
        _logger = logger;
        _clientState = clientState;
        _objectTable = objectTable;
        _framework = framework;
        _gameGui = gameGui;
        _condition = condition;
        _gameData = gameData;
        _gameConfig = gameConfig;
        _playerState = playerState;
        _actorObjectService = actorObjectService;
        _targetManager = targetManager;
        _blockedCharacterHandler = blockedCharacterHandler;
        Mediator = mediator;
        _performanceCollector = performanceCollector;
        _configService = configService;
        _playerPerformanceConfigService = playerPerformanceConfigService;
        _pairFactory = pairFactory;
        var clientLanguage = _clientState.ClientLanguage;
        WorldData = new(() =>
        {
            return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)!
                .Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])
                    || w is { RowId: > 1000, Region: 101 or 201 }))
                .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
        });
        JobData = new(() =>
        {
            return gameData.GetExcelSheet<ClassJob>(clientLanguage)!
                .ToDictionary(k => k.RowId, k => k.Name.ToString());
        });
        TerritoryData = new(() => BuildTerritoryData(clientLanguage));
        TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English));
        MapData = new(() => BuildMapData(clientLanguage));
        ContentFinderData = new Lazy<Dictionary<uint, string>>(() =>
        {
            return _gameData.GetExcelSheet<TerritoryType>()!
                .Where(w => w.RowId != 0 && !string.IsNullOrEmpty(w.ContentFinderCondition.ValueNullable?.Name.ToString()))
                .ToDictionary(w => w.RowId, w => w.ContentFinderCondition.Value.Name.ToString());
        });
        mediator.Subscribe<TargetPairMessage>(this, (msg) =>
        {
            if (clientState.IsPvP) return;
            if (!ResolvePairAddress(msg.Pair, out var pair, out var addr)) return;
            var useFocusTarget = _configService.Current.UseFocusTarget;
            _ = RunOnFrameworkThread(() =>
            {
                var gameObject = CreateGameObject(addr);
                if (gameObject is null) return;
                if (useFocusTarget)
                {
                    _targetManager.FocusTarget = gameObject;
                    if (_FocusPairIdent.HasValue && _FocusPairIdent.Value.Equals(pair.UniqueIdent))
                    {
                        _FocusOriginalTarget = _targetManager.FocusTarget;
                    }
                }
                else
                {
                    _targetManager.Target = gameObject;
                }
            }).ConfigureAwait(false);
        });
        IsWine = Util.IsWine();
        _cid = RebuildCID();
    }

    private Lazy<ulong> RebuildCID() => new(GetCID);

    public bool IsWine { get; init; }
    private Dictionary<uint, string> BuildTerritoryData(Dalamud.Game.ClientLanguage language)
    {
        var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
        return _gameData.GetExcelSheet<TerritoryType>(language)!
            .Where(w => w.RowId != 0)
            .ToDictionary(w => w.RowId, w =>
            {
                var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
                var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
                return BuildPlaceName(regionName, placeName, string.Empty);
            });
    }

    private Dictionary<uint, (Map Map, string MapName)> BuildMapData(Dalamud.Game.ClientLanguage language)
    {
        var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
        return _gameData.GetExcelSheet<Map>(language)!
            .Where(w => w.RowId != 0)
            .ToDictionary(w => w.RowId, w =>
            {
                var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
                var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
                var subPlaceName = GetPlaceName(placeNames, w.PlaceNameSub.RowId);
                var displayName = BuildPlaceName(regionName, placeName, subPlaceName);
                return (w, displayName);
            });
    }
    private static string GetPlaceName(Lumina.Excel.ExcelSheet<PlaceName> placeNames, uint rowId)
    {
        if (rowId == 0)
        {
            return string.Empty;
        }

        return placeNames.GetRow(rowId).Name.ToString();
    }

    private static string BuildPlaceName(string regionName, string placeName, string subPlaceName)
    {
        StringBuilder sb = new();
        if (!string.IsNullOrWhiteSpace(regionName))
        {
            sb.Append(regionName);
        }

        if (!string.IsNullOrWhiteSpace(placeName))
        {
            if (sb.Length > 0)
            {
                sb.Append(" - ");
            }
            sb.Append(placeName);
        }

        if (!string.IsNullOrWhiteSpace(subPlaceName))
        {
            if (sb.Length > 0)
            {
                sb.Append(" - ");
            }
            sb.Append(subPlaceName);
        }

        return sb.ToString();
    }
    private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address)
    {
        resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair;
        address = nint.Zero;
        var name = resolvedPair.PlayerName;
        if (string.IsNullOrEmpty(name)) return false;
        if (!_actorObjectService.TryGetPlayerByName(name, out var descriptor))
            return false;
        address = descriptor.Address;
        return address != nint.Zero;
    }

    public void FocusVisiblePair(Pair pair)
    {
        if (_clientState.IsPvP) return;
        if (!ResolvePairAddress(pair, out var resolvedPair, out var address)) return;
        _ = RunOnFrameworkThread(() => FocusPairUnsafe(address, resolvedPair.UniqueIdent));
    }

    public void ReleaseVisiblePairFocus()
    {
        _ = RunOnFrameworkThread(ReleaseFocusUnsafe);
    }

    private void FocusPairUnsafe(nint address, PairUniqueIdentifier pairIdent)
    {
        var target = CreateGameObject(address);
        if (target is null) return;

        if (!_FocusPairIdent.HasValue)
        {
            _FocusOriginalTarget = _targetManager.FocusTarget;
        }

        _targetManager.FocusTarget = target;
        _FocusPairIdent = pairIdent;
    }

    private void ReleaseFocusUnsafe()
    {
        if (!_FocusPairIdent.HasValue)
        {
            return;
        }

        var previous = _FocusOriginalTarget;
        if (previous != null && !IsObjectPresent(previous))
        {
            previous = null;
        }

        _targetManager.FocusTarget = previous;
        _FocusPairIdent = null;
        _FocusOriginalTarget = null;
    }

    public unsafe GameObject* GposeTarget
    {
        get => TargetSystem.Instance()->GPoseTarget;
        set => TargetSystem.Instance()->GPoseTarget = value;
    }

    private unsafe bool HasGposeTarget => GposeTarget != null;
    private unsafe int GPoseTargetIdx => !HasGposeTarget ? -1 : GposeTarget->ObjectIndex;

    public async Task<IGameObject?> GetGposeTargetGameObjectAsync()
    {
        if (!HasGposeTarget)
            return null;

        return await _framework.RunOnFrameworkThread(() => _objectTable[GPoseTargetIdx]).ConfigureAwait(true);
    }
    public bool IsAnythingDrawing { get; private set; } = false;
    public bool IsInCutscene { get; private set; } = false;
    public bool IsInGpose { get; private set; } = false;
    public bool IsGameUiHidden => _gameGui.GameUiHidden;
    public bool IsLoggedIn { get; private set; }
    public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread;
    public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
    public bool IsInCombat { get; private set; } = false;
    public bool IsPerforming { get; private set; } = false;
    public bool IsInInstance { get; private set; } = false;
    public bool IsInDuty => _condition[ConditionFlag.BoundByDuty];
    public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
    public uint ClassJobId => _classJobId!.Value;
    public Lazy<Dictionary<uint, string>> JobData { get; private set; }
    public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
    public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
    public Lazy<Dictionary<uint, string>> TerritoryDataEnglish { get; private set; }
    public Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; }
    public Lazy<Dictionary<uint, string>> ContentFinderData { get; private set; }
    public bool IsLodEnabled { get; private set; }
    public LightlessMediator Mediator { get; }

    public bool IsInFieldOperation
    {
        get
        {
            if (!IsInDuty)
            {
                return false;
            }

            var territoryId = _clientState.TerritoryType;
            if (territoryId == 0)
            {
                return false;
            }

            if (!TerritoryDataEnglish.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name))
            {
                return false;
            }

            return name.Contains("Eureka", StringComparison.OrdinalIgnoreCase)
                || name.Contains("Bozja", StringComparison.OrdinalIgnoreCase)
                || name.Contains("Zadnor", StringComparison.OrdinalIgnoreCase);
        }
    }

    public IGameObject? CreateGameObject(IntPtr reference)
    {
        EnsureIsOnFramework();
        return _objectTable.CreateObjectReference(reference);
    }

    public async Task<IGameObject?> CreateGameObjectAsync(IntPtr reference)
    {
        return await RunOnFrameworkThread(() => _objectTable.CreateObjectReference(reference)).ConfigureAwait(false);
    }

    public void EnsureIsOnFramework()
    {
        if (!_framework.IsInFrameworkUpdateThread) throw new InvalidOperationException("Can only be run on Framework");
    }

    public ICharacter? GetCharacterFromObjectTableByIndex(int index)
    {
        EnsureIsOnFramework();
        var objTableObj = _objectTable[index];
        if (objTableObj!.ObjectKind != DalamudObjectKind.Player) return null;
        return (ICharacter)objTableObj;
    }

    public unsafe IntPtr GetCompanionPtr(IntPtr? playerPointer = null)
    {
        EnsureIsOnFramework();
        var mgr = CharacterManager.Instance();
        playerPointer ??= GetPlayerPtr();
        if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero;
        return (IntPtr)mgr->LookupBuddyByOwnerObject((BattleChara*)playerPointer);
    }

    public async Task<IntPtr> GetCompanionAsync(IntPtr? playerPointer = null)
    {
        return await RunOnFrameworkThread(() => GetCompanionPtr(playerPointer)).ConfigureAwait(false);
    }

    public async Task<ICharacter?> GetGposeCharacterFromObjectTableByNameAsync(string name, bool onlyGposeCharacters = false)
    {
        return await RunOnFrameworkThread(() => GetGposeCharacterFromObjectTableByName(name, onlyGposeCharacters)).ConfigureAwait(false);
    }

    public ICharacter? GetGposeCharacterFromObjectTableByName(string name, bool onlyGposeCharacters = false)
    {
        EnsureIsOnFramework();
        return (ICharacter?)_objectTable
            .FirstOrDefault(i => (!onlyGposeCharacters || i.ObjectIndex >= 200) && string.Equals(i.Name.ToString(), name, StringComparison.Ordinal));
    }

    public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
    {
        foreach (var actor in _objectTable
                     .Where(a => a.ObjectIndex > 200 && a.ObjectKind == DalamudObjectKind.Player))
        {
            var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter;
            if (character != null)
                yield return character;
        }
    }

    public bool GetIsPlayerPresent()
    {
        EnsureIsOnFramework();
        return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid() && _playerState.IsLoaded;
    }

    public async Task<bool> GetIsPlayerPresentAsync()
    {
        return await RunOnFrameworkThread(GetIsPlayerPresent).ConfigureAwait(false);
    }

    public unsafe IntPtr GetMinionOrMountPtr(IntPtr? playerPointer = null)
    {
        EnsureIsOnFramework();
        playerPointer ??= GetPlayerPtr();
        if (playerPointer == IntPtr.Zero) return IntPtr.Zero;

        var playerAddress = playerPointer.Value;
        var ownerEntityId = ((Character*)playerAddress)->EntityId;
        var candidateAddress = _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
        if (ownerEntityId == 0) return candidateAddress;

        if (playerAddress == _actorObjectService.LocalPlayerAddress)
        {
            var localOwned = _actorObjectService.LocalMinionOrMountAddress;
            if (localOwned != nint.Zero)
            {
                return localOwned;
            }
        }

        if (candidateAddress != nint.Zero)
        {
            var candidate = (GameObject*)candidateAddress;
            var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
            if ((candidateKind == DalamudObjectKind.MountType || candidateKind == DalamudObjectKind.Companion)
                && ResolveOwnerId(candidate) == ownerEntityId)
            {
                return candidateAddress;
            }
        }

        var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
            kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
        if (ownedObject != nint.Zero)
        {
            return ownedObject;
        }

        return candidateAddress;
    }

    public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
    {
        return await RunOnFrameworkThread(() => GetMinionOrMountPtr(playerPointer)).ConfigureAwait(false);
    }

    public unsafe IntPtr GetPetPtr(IntPtr? playerPointer = null)
    {
        EnsureIsOnFramework();
        if (_classJobIdsIgnoredForPets.Contains(_classJobId ?? 0)) return IntPtr.Zero;
        var mgr = CharacterManager.Instance();
        playerPointer ??= GetPlayerPtr();
        if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero;

        var ownerAddress = playerPointer.Value;
        var ownerEntityId = ((Character*)ownerAddress)->EntityId;
        if (ownerEntityId == 0) return IntPtr.Zero;

        var candidate = (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)ownerAddress);
        if (candidate != IntPtr.Zero)
        {
            var candidateObj = (GameObject*)candidate;
            if (IsPetMatch(candidateObj, ownerEntityId))
            {
                return candidate;
            }
        }

        return FindOwnedPet(ownerEntityId, ownerAddress);
    }

    public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
    {
        return await RunOnFrameworkThread(() => GetPetPtr(playerPointer)).ConfigureAwait(false);
    }

    private unsafe nint FindOwnedObject(uint ownerEntityId, nint ownerAddress, Func<DalamudObjectKind, bool> matchesKind)
    {
        if (ownerEntityId == 0)
        {
            return nint.Zero;
        }

        foreach (var obj in _objectTable)
        {
            if (obj is null || obj.Address == nint.Zero || obj.Address == ownerAddress)
            {
                continue;
            }

            if (!matchesKind(obj.ObjectKind))
            {
                continue;
            }

            var candidate = (GameObject*)obj.Address;
            if (ResolveOwnerId(candidate) == ownerEntityId)
            {
                return obj.Address;
            }
        }

        return nint.Zero;
    }

    private unsafe nint FindOwnedPet(uint ownerEntityId, nint ownerAddress)
    {
        if (ownerEntityId == 0)
        {
            return nint.Zero;
        }

        foreach (var obj in _objectTable)
        {
            if (obj is null || obj.Address == nint.Zero || obj.Address == ownerAddress)
            {
                continue;
            }

            if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
            {
                continue;
            }

            var candidate = (GameObject*)obj.Address;
            if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
            {
                continue;
            }

            if (ResolveOwnerId(candidate) == ownerEntityId)
            {
                return obj.Address;
            }
        }

        return nint.Zero;
    }

    private static unsafe bool IsPetMatch(GameObject* candidate, uint ownerEntityId)
    {
        if (candidate == null)
        {
            return false;
        }

        if ((DalamudObjectKind)candidate->ObjectKind != DalamudObjectKind.BattleNpc)
        {
            return false;
        }

        if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
        {
            return false;
        }

        return ResolveOwnerId(candidate) == ownerEntityId;
    }

    private static unsafe uint ResolveOwnerId(GameObject* gameObject)
    {
        if (gameObject == null)
        {
            return 0;
        }

        if (gameObject->OwnerId != 0)
        {
            return gameObject->OwnerId;
        }

        var character = (Character*)gameObject;
        if (character == null)
        {
            return 0;
        }

        if (character->CompanionOwnerId != 0)
        {
            return character->CompanionOwnerId;
        }

        var parent = character->GetParentCharacter();
        return parent != null ? parent->EntityId : 0;
    }

    public async Task<IPlayerCharacter> GetPlayerCharacterAsync()
    {
        return await RunOnFrameworkThread(GetPlayerCharacter).ConfigureAwait(false);
    }

    public IPlayerCharacter GetPlayerCharacter()
    {
        EnsureIsOnFramework();
        return _objectTable.LocalPlayer!;
    }

    public IntPtr GetPlayerCharacterFromCachedTableByIdent(string characterName)
    {
        if (_actorObjectService.TryGetValidatedActorByHash(characterName, out var actor))
            return actor.Address;
        return IntPtr.Zero;
    }

    public string GetPlayerName()
    {
        return _playerState.CharacterName;
    }

    public unsafe ulong GetCID()
    {
        return _playerState.ContentId;
    }

    public string GetPlayerNameHashed()
    {
        return _cid.Value.ToString().GetHash256();
    }

    public static unsafe bool TryGetHashedCID(IPlayerCharacter? playerCharacter, out string hashedCid)
    {
        hashedCid = string.Empty;
        if (playerCharacter == null)
            return false;

        var address = playerCharacter.Address;
        if (address == nint.Zero)
            return false;

        var cid = ((BattleChara*)address)->Character.ContentId;
        if (cid == 0)
            return false;

        hashedCid = cid.ToString().GetHash256();
        return true;
    }

    public unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
    {
        return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256();
    }

    public IntPtr GetPlayerPtr()
    {
        EnsureIsOnFramework();
        return _objectTable.LocalPlayer?.Address ?? IntPtr.Zero;
    }

    public async Task<IntPtr> GetPlayerPointerAsync()
    {
        return await RunOnFrameworkThread(GetPlayerPtr).ConfigureAwait(false);
    }

    public uint GetHomeWorldId()
    {
        return _playerState.HomeWorld.RowId;
    }

    public uint GetWorldId()
    {
        return _playerState.CurrentWorld.RowId;
    }

    public unsafe LocationInfo GetMapData()
    {
        var houseMan = HousingManager.Instance();

        var location = new LocationInfo();
        location.ServerId = _playerState.CurrentWorld.RowId;
        location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; 
        location.TerritoryId = _clientState.TerritoryType;
        location.MapId = _clientState.MapId;
        if (houseMan != null)
        {
            if (houseMan->IsInside())
            {
                location.TerritoryId = HousingManager.GetOriginalHouseTerritoryTypeId();
                var house = houseMan->GetCurrentIndoorHouseId();
                location.WardId = house.WardIndex + 1u;
                location.HouseId = house.IsApartment ? 100 : house.PlotIndex + 1u;
                location.RoomId = (uint)house.RoomNumber;
                location.DivisionId = house.IsApartment ? house.ApartmentDivision + 1u : houseMan->GetCurrentDivision();
            }
            else if (houseMan->IsInWorkshop())
            {
                var workShop = houseMan->WorkshopTerritory;
                var house = workShop->HouseId;
                location.WardId = house.WardIndex + 1u;
                location.HouseId = house.PlotIndex + 1u;
            }
            else if (houseMan->IsOutside())
            {
                var outside = houseMan->OutdoorTerritory;
                var house = outside->HouseId;
                location.WardId = house.WardIndex + 1u;
                //location.HouseId = (uint)houseMan->GetCurrentPlot() + 1;
                location.DivisionId = houseMan->GetCurrentDivision();
            }
            //_logger.LogWarning(LocationToString(location));
        }
        return location;
    }
    
    public string LocationToString(LocationInfo location)
    {
        if (location.ServerId is 0 || location.TerritoryId is 0) return String.Empty;
        var str = WorldData.Value[(ushort)location.ServerId];

        if (ContentFinderData.Value.TryGetValue(location.TerritoryId , out var dutyName))
        {
            str += $" - [In Duty]{dutyName}";
        }
        else
        {
            if (location.HouseId is not 0 || location.MapId is 0) // Dont show mapName when in house/no map available
            {
                str += $" - {TerritoryData.Value[(ushort)location.TerritoryId]}";
            }
            else
            {
                str += $" - {MapData.Value[(ushort)location.MapId].MapName}";
            }

            if (location.InstanceId is not 0)
            {
                str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
            }

            if (location.WardId is not 0)
            {
                str += $" Ward #{location.WardId}";
            }

            if (location.HouseId is not 0 and not 100)
            {
                str += $" House #{location.HouseId}";
            }
            else if (location.HouseId is 100)
            {
                str += $" {(location.DivisionId == 2 ? "[Subdivision]" : "")} Apartment";
            }

            if (location.RoomId is not 0)
            {
                str += $" Room #{location.RoomId}";
            }
        }

        return str;
    }

    public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map)
    {
        EnsureIsOnFramework();
        var agentMap = AgentMap.Instance();
        if (agentMap == null) return;
        agentMap->OpenMapByMapId(map.RowId);
        agentMap->SetFlagMapMarker(map.TerritoryType.RowId, map.RowId, position);
    }

    public unsafe bool IsGameObjectPresent(IntPtr key)
    {
        return _objectTable.Any(f => f.Address == key);
    }

    public bool IsObjectPresent(IGameObject? obj)
    {
        EnsureIsOnFramework();
        return obj != null && obj.IsValid();
    }

    public async Task<bool> IsObjectPresentAsync(IGameObject? obj)
    {
        return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false);
    }

    public IPlayerCharacter? GetPlayerByNameAndWorld(string name, ushort homeWorldId)
    {
        EnsureIsOnFramework();
        return _objectTable
            .OfType<IPlayerCharacter>()
            .FirstOrDefault(p =>
                string.Equals(p.Name.TextValue, name, StringComparison.Ordinal) &&
                p.HomeWorld.RowId == homeWorldId);
    }

    public async Task RunOnFrameworkThread(System.Action act, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
    {
        var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
        await _performanceCollector.LogPerformance(this, $"RunOnFramework:Act/{fileName}>{callerMember}:{callerLineNumber}", async () =>
        {
            if (_framework.IsInFrameworkUpdateThread)
            {
                act();
                return;
            }

            await _framework.RunOnFrameworkThread(act).ConfigureAwait(false);
        }).ConfigureAwait(false);
    }

    public async Task<T> RunOnFrameworkThread<T>(Func<T> func, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
    {
        var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
        return await _performanceCollector.LogPerformance(this, $"RunOnFramework:Func<{typeof(T)}>/{fileName}>{callerMember}:{callerLineNumber}", async () =>
        {
            if (_framework.IsInFrameworkUpdateThread)
            {
                return func.Invoke();
            }

            return await _framework.RunOnFrameworkThread(func).ConfigureAwait(false);
        }).ConfigureAwait(false);
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting DalamudUtilService");
        _framework.Update += FrameworkOnUpdate;
        if (IsLoggedIn)
        {
            _classJobId = _objectTable.LocalPlayer!.ClassJob.RowId;
        }

        _logger.LogInformation("Started DalamudUtilService");
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogTrace("Stopping {type}", GetType());

        Mediator.UnsubscribeAll(this);
        _framework.Update -= FrameworkOnUpdate;
        if (_FocusPairIdent.HasValue)
        {
            if (_framework.IsInFrameworkUpdateThread)
            {
                ReleaseFocusUnsafe();
            }
            else
            {
                _ = RunOnFrameworkThread(ReleaseFocusUnsafe);
            }
        }
        return Task.CompletedTask;
    }

    public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
    {
        if (!_clientState.IsLoggedIn) return;

        if (ct == null)
            ct = CancellationToken.None;

        const int tick = 250;
        int curWaitTime = 0;
        try
        {
            logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
            await Task.Delay(tick, ct.Value).ConfigureAwait(true);
            curWaitTime += tick;

            while ((!ct.Value.IsCancellationRequested)
                   && curWaitTime < timeOut
                   && await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) // 0b100000000000 is "still rendering" or something
            {
                logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
                curWaitTime += tick;
                await Task.Delay(tick, ct.Value).ConfigureAwait(true);
            }

            logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime);
        }
        catch (AccessViolationException ex)
        {
            logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
        }
    }

    public unsafe void WaitWhileGposeCharacterIsDrawing(IntPtr characterAddress, int timeOut = 5000)
    {
        Thread.Sleep(500);
        var obj = (GameObject*)characterAddress;
        const int tick = 250;
        int curWaitTime = 0;
        _logger.LogTrace("RenderFlags: {flags}", obj->RenderFlags.ToString("X"));
        while (obj->RenderFlags != VisibilityFlags.None && curWaitTime < timeOut)
        {
            _logger.LogTrace($"Waiting for gpose actor to finish drawing");
            curWaitTime += tick;
            Thread.Sleep(tick);
        }
        Thread.Sleep(tick * 2);
    }

    public Vector2 WorldToScreen(IGameObject? obj)
    {
        if (obj == null) return Vector2.Zero;
        return _gameGui.WorldToScreen(obj.Position, out var screenPos) ? screenPos : Vector2.Zero;
    }

    internal (string Name, nint Address) FindPlayerByNameHash(string ident)
    {
        if (_actorObjectService.TryGetValidatedActorByHash(ident, out var descriptor))
        {
            return (descriptor.Name, descriptor.Address);
        }

        return default;
    }

    public string? GetWorldNameFromPlayerAddress(nint address)
    {
        if (address == nint.Zero) return null;
        
        EnsureIsOnFramework();
        var playerCharacter = _objectTable.OfType<IPlayerCharacter>().FirstOrDefault(p => p.Address == address);
        if (playerCharacter == null) return null;
        
        var worldId = (ushort)playerCharacter.HomeWorld.RowId;
        return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
    }

    private unsafe void CheckCharacterForDrawing(nint address, string characterName)
    {
        var gameObj = (GameObject*)address;
        var drawObj = gameObj->DrawObject;
        bool isDrawing = false;
        bool isDrawingChanged = false;
        if ((nint)drawObj != IntPtr.Zero)
        {
            isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
            if (!isDrawing)
            {
                isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
                if (!isDrawing)
                {
                    isDrawing = ((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0;
                    if (isDrawing && !string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
                        && !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal))
                    {
                        _lastGlobalBlockPlayer = characterName;
                        _lastGlobalBlockReason = "HasModelFilesInSlotLoaded";
                        isDrawingChanged = true;
                    }
                }
                else
                {
                    if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
                        && !string.Equals(_lastGlobalBlockReason, "HasModelInSlotLoaded", StringComparison.Ordinal))
                    {
                        _lastGlobalBlockPlayer = characterName;
                        _lastGlobalBlockReason = "HasModelInSlotLoaded";
                        isDrawingChanged = true;
                    }
                }
            }
            else
            {
                if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
                    && !string.Equals(_lastGlobalBlockReason, "RenderFlags", StringComparison.Ordinal))
                {
                    _lastGlobalBlockPlayer = characterName;
                    _lastGlobalBlockReason = "RenderFlags";
                    isDrawingChanged = true;
                }
            }
        }

        if (isDrawingChanged)
        {
            _logger.LogTrace("Global draw block: START => {name} ({reason})", characterName, _lastGlobalBlockReason);
        }

        IsAnythingDrawing |= isDrawing;
    }

    private void FrameworkOnUpdate(IFramework framework)
    {
        _performanceCollector.LogPerformance(this, $"FrameworkOnUpdate", FrameworkOnUpdateInternal);
    }

    private unsafe void FrameworkOnUpdateInternal()
    {
        if ((_objectTable.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
        {
            return;
        }

        bool isNormalFrameworkUpdate = DateTime.UtcNow < _delayedFrameworkUpdateCheck.AddSeconds(1);

        _performanceCollector.LogPerformance(this, $"FrameworkOnUpdateInternal+{(isNormalFrameworkUpdate ? "Regular" : "Delayed")}", () =>
        {
            IsAnythingDrawing = false;
            _performanceCollector.LogPerformance(this, $"TrackedActorsToState",
                () =>
                {
                    if (!_actorObjectService.HooksActive || !isNormalFrameworkUpdate || _actorObjectService.HasPendingHashResolutions)
                    {
                        _actorObjectService.RefreshTrackedActors();
                    }

                    var playerDescriptors = _actorObjectService.PlayerDescriptors;
                    for (var i = 0; i < playerDescriptors.Count; i++)
                    {
                        var actor = playerDescriptors[i];

                        var playerAddress = actor.Address;
                        if (playerAddress == nint.Zero)
                            continue;

                        if (actor.ObjectIndex >= 200)
                            continue;

                        if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, out bool firstTime) && firstTime)
                        {
                            _logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X"));
                            continue;
                        }

                        if (!IsAnythingDrawing)
                        {
                            var gameObj = (GameObject*)playerAddress;
                            var currentName = gameObj != null ? gameObj->NameString ?? string.Empty : string.Empty;
                            var charaName = string.IsNullOrEmpty(currentName) ? actor.Name : currentName;
                            CheckCharacterForDrawing(playerAddress, charaName);
                            if (IsAnythingDrawing)
                                break;
                        }
                        else
                        {
                            break;
                        }
                    }
                });

            if (!IsAnythingDrawing && !string.IsNullOrEmpty(_lastGlobalBlockPlayer))
            {
                _logger.LogTrace("Global draw block: END => {name}", _lastGlobalBlockPlayer);
                _lastGlobalBlockPlayer = string.Empty;
                _lastGlobalBlockReason = string.Empty;
            }

            // Checks on conditions
            var shouldBeInGpose = _clientState.IsGPosing;
            var shouldBeInCombat = _condition[ConditionFlag.InCombat] && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat;
            var shouldBePerforming = _condition[ConditionFlag.Performing] && _playerPerformanceConfigService.Current.PauseWhilePerforming;
            var shouldBeInInstance = _condition[ConditionFlag.BoundByDuty] && _playerPerformanceConfigService.Current.PauseInInstanceDuty;
            var shouldBeInCutscene = _condition[ConditionFlag.WatchingCutscene];

            // Gpose
            HandleStateTransition(() => IsInGpose, v => IsInGpose = v, shouldBeInGpose, "Gpose",
                onEnter: () =>
                {
                    Mediator.Publish(new GposeStartMessage());
                },
                onExit: () =>
                {
                    Mediator.Publish(new GposeEndMessage());
                });

            // Combat
            HandleStateTransition(() => IsInCombat, v => IsInCombat = v, shouldBeInCombat, "Combat",
                onEnter: () =>
                {
                    Mediator.Publish(new CombatStartMessage());
                    Mediator.Publish(new HaltScanMessage(nameof(IsInCombat)));
                },
                onExit: () =>
                {
                    Mediator.Publish(new CombatEndMessage());
                    Mediator.Publish(new ResumeScanMessage(nameof(IsInCombat)));
                });

            // Performance
            HandleStateTransition(() => IsPerforming, v => IsPerforming = v, shouldBePerforming, "Performance",
                onEnter: () =>
                {
                    Mediator.Publish(new PerformanceStartMessage());
                    Mediator.Publish(new HaltScanMessage(nameof(IsPerforming)));
                },
                onExit: () =>
                {
                    Mediator.Publish(new PerformanceEndMessage());
                    Mediator.Publish(new ResumeScanMessage(nameof(IsPerforming)));
                });

            // Instance / Duty
            HandleStateTransition(() => IsInInstance, v => IsInInstance = v, shouldBeInInstance, "Instance",
                onEnter: () =>
                {
                    Mediator.Publish(new InstanceOrDutyStartMessage());
                    Mediator.Publish(new HaltScanMessage(nameof(IsInInstance)));
                },
                onExit: () =>
                {
                    Mediator.Publish(new InstanceOrDutyEndMessage());
                    Mediator.Publish(new ResumeScanMessage(nameof(IsInInstance)));
                });

            // Cutscene
            HandleStateTransition(() => IsInCutscene,v => IsInCutscene = v, shouldBeInCutscene, "Cutscene",
                onEnter: () =>
                {
                    Mediator.Publish(new CutsceneStartMessage());
                    Mediator.Publish(new HaltScanMessage(nameof(IsInCutscene)));
                },
                onExit: () =>
                {
                    Mediator.Publish(new CutsceneEndMessage());
                    Mediator.Publish(new ResumeScanMessage(nameof(IsInCutscene)));
                });

            if (IsInCutscene)
            {
                Mediator.Publish(new CutsceneFrameworkUpdateMessage());
                return;
            }

            if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51])
            {
                var zone = _clientState.TerritoryType;
                if (_lastZone != zone)
                {
                    _lastZone = zone;
                    if (!_sentBetweenAreas)
                    {
                        _logger.LogDebug("Zone switch start");
                        _sentBetweenAreas = true;
                        Mediator.Publish(new ZoneSwitchStartMessage());
                        Mediator.Publish(new HaltScanMessage(nameof(ConditionFlag.BetweenAreas)));
                    }
                }

                return;
            }

            if (_sentBetweenAreas)
            {
                _logger.LogDebug("Zone switch end");
                _sentBetweenAreas = false;
                Mediator.Publish(new ZoneSwitchEndMessage());
                Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas)));
            }
            
            //Map
            if (!_sentBetweenAreas)
            {
                var mapid = _clientState.MapId;
                if (mapid != _lastMapId)
                {
                    _lastMapId = mapid;
                    Mediator.Publish(new MapChangedMessage(mapid));
                }
            }
            

            var localPlayer = _objectTable.LocalPlayer;
            if (localPlayer != null)
            {
                _classJobId = localPlayer.ClassJob.RowId;

                var currentWorldId = (ushort)localPlayer.CurrentWorld.RowId;
                if (currentWorldId != _lastWorldId)
                {
                    var previousWorldId = _lastWorldId;
                    _lastWorldId = currentWorldId;
                    Mediator.Publish(new WorldChangedMessage(previousWorldId, currentWorldId));
                }
            }
            else if (_lastWorldId != 0)
            {
                _lastWorldId = 0;
            }

            if (!IsInCombat || !IsPerforming || !IsInInstance)
                Mediator.Publish(new FrameworkUpdateMessage());

            Mediator.Publish(new PriorityFrameworkUpdateMessage());

            if (isNormalFrameworkUpdate)
                return;

            if (localPlayer != null && !IsLoggedIn)
            {
                _logger.LogDebug("Logged in");
                IsLoggedIn = true;
                _lastZone = _clientState.TerritoryType;
                _lastWorldId = (ushort)localPlayer.CurrentWorld.RowId;
                _cid = RebuildCID();
                Mediator.Publish(new DalamudLoginMessage());
            }
            else if (localPlayer == null && IsLoggedIn)
            {
                _logger.LogDebug("Logged out");
                IsLoggedIn = false;
                _lastWorldId = 0;
                Mediator.Publish(new DalamudLogoutMessage());
            }

            if (_gameConfig != null
                && _gameConfig.TryGet(Dalamud.Game.Config.SystemConfigOption.LodType_DX11, out bool lodEnabled))
            {
                IsLodEnabled = lodEnabled;
            }

            if (IsInCombat || IsPerforming || IsInInstance)
                Mediator.Publish(new FrameworkUpdateMessage());

            Mediator.Publish(new DelayedFrameworkUpdateMessage());

            _delayedFrameworkUpdateCheck = DateTime.UtcNow;
        });
    }

    /// <summary>
    /// Handler for the transition of different states of game
    /// </summary>
    /// <param name="getState">Get state of condition</param>
    /// <param name="setState">Set state of condition</param>
    /// <param name="shouldBeActive">Correction of the state of the condition</param>
    /// <param name="stateName">Condition name</param>
    /// <param name="onEnter">Function for on entering the state</param>
    /// <param name="onExit">Function for on leaving the state</param>
    private void HandleStateTransition(Func<bool> getState, Action<bool> setState, bool shouldBeActive, string stateName, System.Action onEnter, System.Action onExit)
    {
        var isActive = getState();

        if (shouldBeActive && !isActive)
        {
            _logger.LogDebug("{stateName} start", stateName);
            setState(true);
            onEnter();
        }
        else if (!shouldBeActive && isActive)
        {
            _logger.LogDebug("{stateName} end", stateName);
            setState(false);
            onExit();
        }
    }
}
