using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Plugin.Services;
using HSUI.Config;
using HSUI.Enums;
using HSUI.Interface.GeneralElements;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using StructsCharacter = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
using StructsCharacterManager = FFXIVClientStructs.FFXIV.Client.Game.Character.CharacterManager;
using StructsGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;

namespace HSUI.Helpers
{
    internal static class Utils
    {
        private static uint InvalidGameObjectId = 0xE0000000;

        public static IGameObject? GetBattleChocobo(IGameObject? player)
        {
            if (player == null)
            {
                return null;
            }

            return GetBuddy(player.GameObjectId, BattleNpcSubKind.Chocobo);
        }

        public static IGameObject? GetBuddy(ulong ownerId, BattleNpcSubKind kind)
        {
            // only the first 200 elements in the array are relevant due to the order in which SE packs data into the array
            // we do a step of 2 because its always an actor followed by its companion
            for (var i = 0; i < 200; i += 2)
            {
                var gameObject = Plugin.ObjectTable[i];

                if (gameObject == null || gameObject.GameObjectId == InvalidGameObjectId || gameObject is not IBattleNpc battleNpc)
                {
                    continue;
                }

                if (battleNpc.BattleNpcKind == kind && battleNpc.OwnerId == ownerId)
                {
                    return gameObject;
                }
            }

            return null;
        }

        public static IGameObject? GetGameObjectByName(string name)
        {
            // only the first 200 elements in the array are relevant due to the order in which SE packs data into the array
            // we do a step of 2 because its always an actor followed by its companion
            for (int i = 0; i < 200; i += 2)
            {
                IGameObject? gameObject = Plugin.ObjectTable[i];

                if (gameObject == null || gameObject.GameObjectId == InvalidGameObjectId || gameObject.GameObjectId == 0)
                {
                    continue;
                }

                if (gameObject.Name.ToString() == name)
                {
                    return gameObject;
                }
            }

            return null;
        }

        public static unsafe bool IsHostile(IGameObject obj)
        {
            StructsGameObject* gameObject = (StructsGameObject*)obj.Address;
            byte plateType = gameObject->GetNamePlateColorType();

            // 4, 5, 6: Enemy players in PvP
            // 7: yellow, can be attacked, not engaged
            // 8: dead
            // 9: red, engaged with your party
            // 10: purple, engaged with other party
            // 11: orange, aggro'd to your party but not attacked yet
            return plateType >= 4 && plateType <= 11;
        }

        /// <summary>Returns Grand Company icon ID (Maelstrom, Flames, or Adders) for enemy players in PvP Frontline.
        /// Nameplate color types 4, 5, 6 map to the three teams. Returns null when not a PvP enemy team (4–6).
        /// Use iconOverrides for custom IDs: [0]=team1(plate4), [1]=team2(plate5), [2]=team3(plate6). Non-zero overrides default.</summary>
        public static unsafe uint? GrandCompanyIconIdForPvPEnemy(IGameObject? obj, (int t1, int t2, int t3)? iconOverrides = null)
        {
            if (obj == null || !Plugin.ClientState.IsPvP) return null;
            StructsGameObject* gameObject = (StructsGameObject*)obj.Address;
            byte plateType = gameObject->GetNamePlateColorType();
            if (plateType < 4 || plateType > 6) return null;

            // 62601=Maelstrom, 62602=Twin Adder, 62603=Immortal Flames
            uint iconId = plateType switch
            {
                4 => iconOverrides.HasValue && iconOverrides.Value.t1 > 0 ? (uint)iconOverrides.Value.t1 : 62601u,
                5 => iconOverrides.HasValue && iconOverrides.Value.t2 > 0 ? (uint)iconOverrides.Value.t2 : 62602u,
                6 => iconOverrides.HasValue && iconOverrides.Value.t3 > 0 ? (uint)iconOverrides.Value.t3 : 62603u,
                _ => 0
            };
            return iconId > 0 ? iconId : null;
        }

        public static unsafe float ActorShieldValue(IGameObject? actor)
        {
            if (actor == null || actor is not ICharacter)
            {
                return 0f;
            }

            StructsCharacter* chara = (StructsCharacter*)actor.Address;
            return Math.Min(chara->CharacterData.ShieldValue, 100f) / 100f;
        }

        public static bool IsActorCasting(IGameObject? actor)
        {
            if (actor is not IBattleChara chara)
            {
                return false;
            }

            try
            {
                return chara.IsCasting;
            }
            catch { }

            return false;
        }

        public static IEnumerable<IStatus> StatusListForActor(IGameObject? obj)
        {
            if (obj is IBattleChara chara)
            {
                return StatusListForBattleChara(chara);
            }

            return new List<IStatus>();
        }

        public static IEnumerable<IStatus> StatusListForBattleChara(IBattleChara? chara)
        {
            List<IStatus> statusList = new List<IStatus>();
            if (chara == null)
            {
                return statusList;
            }

            try
            {
                statusList = chara.StatusList.ToList();
            }
            catch { }

            return statusList;
        }

        public static string DurationToString(double duration, int decimalCount = 0)
        {
            if (duration == 0)
            {
                return "";
            }

            TimeSpan t = TimeSpan.FromSeconds(duration);

            if (t.Hours >= 1) { return t.Hours + "h"; }
            if (t.Minutes >= 5) { return t.Minutes + "m"; }
            if (t.Minutes >= 1) { return $"{t.Minutes}:{t.Seconds:00}"; }

            return duration.ToString("N" + decimalCount, ConfigurationManager.Instance.ActiveCultreInfo);
        }

        public static IStatus? GetTankInvulnerabilityID(IBattleChara actor)
        {
            return StatusListForBattleChara(actor).FirstOrDefault(o => o.StatusId is 810 or 811 or 3255 or 1302 or 409 or 1836 or 82);
        }

        public static bool IsOnCleanseJob()
        {
            IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer;

            return player != null && JobsHelper.IsJobWithCleanse(player.ClassJob.RowId, player.Level);
        }

        public static IGameObject? FindTargetOfTarget(IGameObject? target, IGameObject? player, IObjectTable actors)
        {
            if (target == null)
            {
                return null;
            }

            // Dalamud for now has an issue where it is only able to get the target ID of
            // NON-Networked objects through anything but GetTargetId on ClientStruct Gameobjects.
            // The bypass converts all Dalamud GameObject Data to ClientStructs GameObject Data and handles it accordingly.
            int actualTargetId = GetActualTargetId(target);
            // The Object ID that gets returned from minions is in reality the index
            // Checking for the correct object ID wouldn't work anyways as you would yet again run into the ObjectID = 0xE0000000 issue
            if (actualTargetId >= 0 && actualTargetId < actors.Length)
            {
                return actors[actualTargetId];
            }

            if (target.TargetObjectId == 0 && player != null && player.TargetObjectId == 0)
            {
                return player;
            }

            // only the first 200 elements in the array are relevant due to the order in which SE packs data into the array
            // we do a step of 2 because its always an actor followed by its companion
            for (int i = 0; i < 200; i += 2)
            {
                IGameObject? actor = actors[i];
                if (actor?.GameObjectId == target.TargetObjectId)
                {
                    return actor;
                }
            }

            return null;
        }

        /// <summary>
        /// Gets the actual target ID of your targets target.
        /// </summary>
        /// <param name="target">Your target</param>
        /// <returns>Target ID of your targets targer. Returns -1 if old code should be ran.</returns>
        private static unsafe int GetActualTargetId(IGameObject target)
        {
            // We only need to check for companions.
            // Why not check target.TargetObject?.ObjectKind == ObjectKind.Companion?
            // Due to the Non-Networked game object bug the game is unaware of what type the object should actually be
            if (target.TargetObject?.ObjectKind != ObjectKind.Player)
            {
                return -1;
            }

            // Here we get the ClientStruct Character of our target (aka the player we are targeting)
            StructsCharacter targetChara = StructsCharacterManager.Instance()->LookupBattleCharaByEntityId(target.EntityId)->Character;

            // This method is key. GetTargetId() returns the targets player target ID. If it is converted to a hex string and starts with the number 4, it is a minion.
            // Even though it is a minion, it still returns the players target ID.
            ulong realTargetID = targetChara.GetTargetId();
            if (!realTargetID.ToString("X").StartsWith("4"))
            {
                return -1;
            }

            // We look up the parents ClientStruct GameObject
            StructsCharacter* realBattleChara = (StructsCharacter*)StructsCharacterManager.Instance()->LookupBattleCharaByEntityId((uint)realTargetID);
            if (realBattleChara == null)
            {
                return -1;
            }

            // And get the companion off of that
            StructsGameObject* companionGameObject = (StructsGameObject*)realBattleChara->CompanionData.CompanionObject;
            if (companionGameObject == null)
            {
                return -1;
            }

            // We return the index of the object here. Why?
            // Again due to the bug where ObjectID = 0xE0000000
            // The index does work and returns the exact minion index.
            return companionGameObject->ObjectIndex;
        }

        public static Vector2 GetAnchoredPosition(Vector2 position, Vector2 size, DrawAnchor anchor)
        {
            return anchor switch
            {
                DrawAnchor.Center => position - size / 2f,
                DrawAnchor.Left => position + new Vector2(0, -size.Y / 2f),
                DrawAnchor.Right => position + new Vector2(-size.X, -size.Y / 2f),
                DrawAnchor.Top => position + new Vector2(-size.X / 2f, 0),
                DrawAnchor.TopLeft => position,
                DrawAnchor.TopRight => position + new Vector2(-size.X, 0),
                DrawAnchor.Bottom => position + new Vector2(-size.X / 2f, -size.Y),
                DrawAnchor.BottomLeft => position + new Vector2(0, -size.Y),
                DrawAnchor.BottomRight => position + new Vector2(-size.X, -size.Y),
                _ => position
            };
        }

        public static string UserFriendlyConfigName(string configTypeName) => UserFriendlyString(configTypeName, "Config");

        public static string UserFriendlyString(string str, string? remove)
        {
            string? s = remove != null ? str.Replace(remove, "") : str;

            Regex? regex = new(@"
                    (?<=[A-Z])(?=[A-Z][a-z]) |
                    (?<=[^A-Z])(?=[A-Z]) |
                    (?<=[A-Za-z])(?=[^A-Za-z])",
                RegexOptions.IgnorePatternWhitespace);

            return regex.Replace(s, " ");
        }

        public static void OpenUrl(string url)
        {
            try
            {
                Process.Start(url);
            }
            catch
            {
                try
                {
                    // hack because of this: https://github.com/dotnet/corefx/issues/10361
                    if (RuntimeInformation.IsOSPlatform(osPlatform: OSPlatform.Windows))
                    {
                        url = url.Replace("&", "^&");
                        Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });
                    }
                    else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
                    {
                        Process.Start("xdg-open", url);
                    }
                }
                catch (Exception e)
                {
                    Plugin.Logger.Error("Error trying to open url: " + e.Message);
                }
            }
        }

        public static unsafe bool? IsTargetCasting()
        {
            AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_TargetInfo", 1).Address;
            if (addon != null && addon->IsVisible)
            {
                AtkImageNode* imageNode = addon->GetImageNodeById(15);
                return imageNode == null || imageNode->IsVisible();
            }

            addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_TargetInfoCastBar", 1).Address;
            if (addon != null && addon->IsVisible)
            {
                AtkImageNode* imageNode = addon->GetImageNodeById(7);
                return imageNode != null || imageNode->IsVisible();
            }

            return null;
        }

        public static unsafe bool? IsFocusTargetCasting()
        {
            AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_FocusTargetInfo", 1).Address;
            if (addon != null && addon->IsVisible)
            {
                AtkTextNode* textNode = addon->GetTextNodeById(5);
                return textNode == null || textNode->IsVisible();
            }

            return null;
        }

        public static unsafe bool? IsEnemyInListCasting(int index)
        {
            if (index < 0 || index > 7) { return null; }

            AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_EnemyList", 1).Address;
            if (addon == null || !addon->IsVisible) { return null; }

            uint buttonId = (index == 0) ? 2u : (uint)(20000 + index);
            AtkComponentButton* button = addon->GetComponentButtonById(buttonId);
            if (button == null || button->AtkResNode == null || !button->AtkResNode->IsVisible())
            {
                return false;
            }

            AtkImageNode* imageNode = button->GetImageNodeById(8);
            return imageNode == null || imageNode->IsVisible();
        }

        public static unsafe uint? SignIconIDForActor(IGameObject? actor)
        {
            if (actor == null)
            {
                return null;
            }

            return SignIconIDForObjectID(actor.GameObjectId);
        }

        public static unsafe uint? SignIconIDForObjectID(ulong objectId)
        {
            MarkingController* markingController = MarkingController.Instance();
            if (objectId == 0 || objectId == InvalidGameObjectId || markingController == null)
            {
                return null;
            }

            for (int i = 0; i < 17; i++)
            {
                if (objectId == markingController->Markers[i])
                {
                    // attack1-5
                    if (i <= 4)
                    {
                        return (uint)(61201 + i);
                    }
                    // attack6-8
                    else if (i >= 14)
                    {
                        return (uint)(61201 + i - 9);
                    }
                    // shapes
                    else if (i >= 10)
                    {
                        return (uint)(61231 + i - 10);
                    }
                    // ignore1-2
                    else if (i >= 8)
                    {
                        return (uint)(61221 + i - 8);
                    }
                    // bind1-3
                    else if (i >= 5)
                    {
                        return (uint)(61211 + i - 5);
                    }
                }
            }

            return null;
        }

        public static bool IsHealthLabel(LabelConfig config)
        {
            return config.GetText().Contains("[health");
        }
    }
}
