using System;
using System.Collections.Generic;
using System.Linq;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic.Helper;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;

namespace NitroxClient.GameLogic;

public class Items
{
    private readonly IPacketSender packetSender;
    private readonly Entities entities;
    public static GameObject PickingUpObject { get; private set; }
    private readonly EntityMetadataManager entityMetadataManager;

    /// <summary>
    /// Whether or not <see cref="Inventory.Pickup"/> is running. It's useful to discriminate between Inventory.Pickup from
    /// a regular <see cref="Pickupable.Pickup"/>
    /// </summary>
    public bool IsInventoryPickingUp;

    public Items(IPacketSender packetSender, Entities entities, EntityMetadataManager entityMetadataManager)
    {
        this.packetSender = packetSender;
        this.entities = entities;
        this.entityMetadataManager = entityMetadataManager;
    }

    public void PickedUp(GameObject gameObject, TechType techType)
    {
        PickingUpObject = gameObject;

        // Try catch to avoid blocking PickingUpObject with a non null value outside of the current context
        try
        {
            // Newly created objects are always placed into the player's inventory.
            if (!Player.main.TryGetNitroxId(out NitroxId playerId))
            {
                Log.ErrorOnce($"[{nameof(Items)}] Player has no id! Could not set parent of picked up item {gameObject.name}.");
                PickingUpObject = null;
                return;
            }

            InventoryItemEntity inventoryItemEntity = ConvertToInventoryEntityUntracked(gameObject, playerId);

            if (inventoryItemEntity.TechType.ToUnity() != techType)
            {
                Log.Warn($"Provided TechType: {techType} is different than the one automatically attributed to the item {inventoryItemEntity.TechType}");
            }

            PickupItem pickupItem = new(inventoryItemEntity);

            if (packetSender.Send(pickupItem))
            {
                Log.Debug($"Picked up item {inventoryItemEntity}");
            }
        }
        catch (Exception exception)
        {
            Log.Error(exception);
        }
        PickingUpObject = null;
    }

    public void Planted(GameObject gameObject, NitroxId parentId)
    {
        InventoryItemEntity inventoryItemEntity = ConvertToInventoryEntityUntracked(gameObject, parentId);

        if (packetSender.Send(new EntitySpawnedByClient(inventoryItemEntity, true)))
        {
            Log.Debug($"Planted item {inventoryItemEntity}");
        }
    }

    /// <summary>
    /// Tracks the object (as dropped) and notifies the server to spawn the item for other players.
    /// </summary>
    public void Dropped(GameObject gameObject, TechType? techType = null)
    {
        techType ??= CraftData.GetTechType(gameObject);
        // there is a theoretical possibility of a stray remote tracking packet that re-adds the monobehavior, this is purely a safety call.
        RemoveAnyRemoteControl(gameObject);

        // WaterParkCreatures need at least one ManagedUpdate to run so their data is correctly refreshed (isMature and timeNextBreed)
        if (gameObject.TryGetComponent(out WaterParkCreature waterParkCreature))
        {
            waterParkCreature.ManagedUpdate();
        }

        NitroxId id = NitroxEntity.GetIdOrGenerateNew(gameObject);
        Optional<EntityMetadata> metadata = entityMetadataManager.Extract(gameObject);
        string classId = gameObject.GetComponent<PrefabIdentifier>().ClassId;

        WorldEntity droppedItem;
        List<Entity> childrenEntities = GetPrefabChildren(gameObject, id, entityMetadataManager).ToList();

        // If the item is dropped in a WaterPark we need to handle it differently
        NitroxId parentId = null;
        if (IsGlobalRootObject(gameObject) || (gameObject.GetComponent<Pickupable>() && TryGetParentWaterParkId(gameObject.transform.parent, out parentId)))
        {
            // We cast it to an entity type that is always seeable by clients
            // therefore, the packet will be redirected to everyone
            droppedItem = new GlobalRootEntity(gameObject.transform.ToLocalDto(), 0, classId, true, id, techType.Value.ToDto(), metadata.OrNull(), parentId, childrenEntities);
        }
        else if (gameObject.TryGetComponent(out OxygenPipe oxygenPipe))
        {
            // We can't spawn an OxygenPipe without its parent and root
            // Dropped patch is called in OxygenPipe.PlaceInWorld which is why OxygenPipe.ghostModel is valid
            IPipeConnection parentConnection = OxygenPipe.ghostModel.GetParent();
            if (parentConnection == null || !parentConnection.GetGameObject() ||
                !parentConnection.GetGameObject().TryGetNitroxId(out NitroxId parentPipeId))
            {
                Log.Error($"Couldn't find a valid reference to the OxygenPipe's parent pipe");
                return;
            }
            IPipeConnection rootConnection = parentConnection.GetRoot();
            if (rootConnection == null || !rootConnection.GetGameObject() ||
                !rootConnection.GetGameObject().TryGetNitroxId(out NitroxId rootPipeId))
            {
                Log.Error($"Couldn't find a valid reference to the OxygenPipe's root pipe");
                return;
            }

            // Updating the local pipe's references to replace the UniqueIdentifier's id by their NitroxEntity's id
            oxygenPipe.rootPipeUID = rootPipeId.ToString();
            oxygenPipe.parentPipeUID = parentPipeId.ToString();

            droppedItem = new OxygenPipeEntity(gameObject.transform.ToWorldDto(), 0, classId, false, id, techType.Value.ToDto(), metadata.OrNull(), null,
                                              childrenEntities, rootPipeId, parentPipeId, parentConnection.GetAttachPoint().ToDto());
        }
        else
        {
            // Generic case
            droppedItem = new(gameObject.transform.ToWorldDto(), 0, classId, false, id, techType.Value.ToDto(), metadata.OrNull(), null, childrenEntities);
        }

        if (packetSender.Send(new EntitySpawnedByClient(droppedItem, true)))
        {
            Log.Debug($"Dropping item: {droppedItem}");
        }
    }

    /// <summary>
    /// Handles objects placed as figures and posters, or LEDLights so that we can spawn them accordingly afterwards.
    /// </summary>
    public void Placed(GameObject gameObject, TechType techType)
    {
        RemoveAnyRemoteControl(gameObject);

        NitroxId id = NitroxEntity.GetIdOrGenerateNew(gameObject);
        Optional<EntityMetadata> metadata = entityMetadataManager.Extract(gameObject);
        string classId = gameObject.GetComponent<PrefabIdentifier>().ClassId;

        List<Entity> childrenEntities = GetPrefabChildren(gameObject, id, entityMetadataManager).ToList();
        WorldEntity placedItem;

        // If the object is dropped in the water, it'll be parented to a CellRoot so we let it as WorldEntity (see Items.Dropped)
        // PlaceTool's object is located under GlobalRoot or under a CellRoot (we differentiate both by giving a different type)
        // Because objects under CellRoots must only spawn when visible while objects under GlobalRoot must be spawned at all times
        switch (gameObject.AliveOrNull())
        {
            case not null when IsGlobalRootObject(gameObject):
                placedItem = new GlobalRootEntity(gameObject.transform.ToWorldDto(), 0, classId, true, id, techType.ToDto(), metadata.OrNull(), null, childrenEntities);
                break;
            case not null when Player.main.AliveOrNull()?.GetCurrentSub().AliveOrNull()?.TryGetNitroxId(out NitroxId parentId) == true:
                placedItem = new GlobalRootEntity(gameObject.transform.ToLocalDto(), 0, classId, true, id, techType.ToDto(), metadata.OrNull(), parentId, childrenEntities);
                break;
            default:
                // If the object is not under a SubRoot nor in GlobalRoot, it'll be under a CellRoot but we still want to remember its state
                placedItem = new PlacedWorldEntity(gameObject.transform.ToWorldDto(), 0, classId, true, id, techType.ToDto(), metadata.OrNull(), null, childrenEntities);
                break;
        }

        if (packetSender.Send(new EntitySpawnedByClient(placedItem, true)))
        {
            Log.Debug($"Placed object: {placedItem}");
        }
    }

    // This function will record any notable children of the dropped item as a PrefabChildEntity.  In this case, a 'notable'
    // child is one that UWE has tagged with a PrefabIdentifier (class id) and has entity metadata that can be extracted. An
    // example would be recording a Battery PrefabChild inside of a Flashlight WorldEntity.
    public static IEnumerable<Entity> GetPrefabChildren(GameObject gameObject, NitroxId parentId, EntityMetadataManager entityMetadataManager)
    {
        foreach (IGrouping<string, PrefabIdentifier> prefabGroup in gameObject.GetAllComponentsInChildren<PrefabIdentifier>()
                                                                              .Where(prefab => prefab.gameObject != gameObject)
                                                                              .GroupBy(prefab => prefab.classId))
        {
            int indexInGroup = 0;

            foreach (PrefabIdentifier prefab in prefabGroup)
            {
                NitroxId id = NitroxEntity.GetIdOrGenerateNew(prefab.gameObject); // We do this here bc a MetadataExtractor could be requiring the id to increment or so
                Optional<EntityMetadata> metadata = entityMetadataManager.Extract(prefab.gameObject);

                if (metadata.HasValue)
                {
                    TechTag techTag = prefab.gameObject.GetComponent<TechTag>();
                    TechType techType = (techTag) ? techTag.type : TechType.None;

                    yield return new PrefabChildEntity(id, prefab.classId, techType.ToDto(), indexInGroup, metadata.Value, parentId);

                    indexInGroup++;
                }
            }
        }
    }

    /// <summary>
    /// Overloads <see cref="ConvertToInventoryItemEntity"/> and removes any tracking on <paramref name="gameObject"/>
    /// </summary>
    private InventoryItemEntity ConvertToInventoryEntityUntracked(GameObject gameObject, NitroxId parentId)
    {
        InventoryItemEntity inventoryItemEntity = ConvertToInventoryItemEntity(gameObject, parentId, entityMetadataManager);

        // Some picked up entities are not known by the server for several reasons.  First it can be picked up via a spawn item command.  Another
        // example is that some obects are not 'real' objects until they are clicked and end up spawning a prefab.  For example, the fire extinguisher
        // in the escape pod (mono: IntroFireExtinguisherHandTarget) or Creepvine seeds (mono: PickupPrefab).  When clicked, these spawn new prefabs
        // directly into the player's inventory.  These will ultimately be registered server side with the above inventoryItemEntity.
        entities.MarkAsSpawned(inventoryItemEntity);

        // We want to remove any remote tracking immediately on pickup as it can cause weird behavior like holding a ghost item still in the world.
        RemoveAnyRemoteControl(gameObject);
        EntityPositionBroadcaster.StopWatchingEntity(inventoryItemEntity.Id);

        return inventoryItemEntity;
    }

    public static InventoryItemEntity ConvertToInventoryItemEntity(GameObject gameObject, NitroxId parentId, EntityMetadataManager entityMetadataManager)
    {
        NitroxId itemId = NitroxEntity.GetIdOrGenerateNew(gameObject); // id may not exist, create if missing
        string classId = gameObject.RequireComponent<PrefabIdentifier>().ClassId;
        TechType techType = gameObject.RequireComponent<Pickupable>().GetTechType();
        Optional<EntityMetadata> metadata = entityMetadataManager.Extract(gameObject);
        List<Entity> children = GetPrefabChildren(gameObject, itemId, entityMetadataManager).ToList();

        InventoryItemEntity inventoryItemEntity = new(itemId, classId, techType.ToDto(), metadata.OrNull(), parentId, children);
        BatteryChildEntityHelper.TryPopulateInstalledBattery(gameObject, inventoryItemEntity.ChildEntities, itemId);

        return inventoryItemEntity;
    }

    /// <summary>
    /// Some items might be remotely simulated if they were dropped by other players.  We'll want to remove
    /// any remote tracking when we actively handle the item.
    /// </summary>
    private void RemoveAnyRemoteControl(GameObject gameObject)
    {
        UnityEngine.Object.Destroy(gameObject.GetComponent<RemotelyControlled>());
    }

    /// <param name="parent">Parent of the GameObject to check</param>
    public static bool TryGetParentWaterPark(Transform parent, out WaterPark waterPark)
    {
        // NB: When dropped in a WaterPark, items are placed under WaterPark/items_root/
        // So we need to search two steps higher to find the WaterPark
        if (parent && parent.parent && parent.parent.TryGetComponent(out waterPark))
        {
            return true;
        }

        waterPark = null;
        return false;
    }


    /// <inheritdoc cref="TryGetParentWaterPark" />
    private static bool TryGetParentWaterParkId(Transform parent, out NitroxId waterParkId)
    {
        if (TryGetParentWaterPark(parent, out WaterPark waterPark) && waterPark.TryGetNitroxId(out waterParkId))
        {
            return true;
        }

        waterParkId = null;
        return false;
    }

    public static List<InstalledModuleEntity> GetEquipmentModuleEntities(Equipment equipment, NitroxId equipmentId, EntityMetadataManager entityMetadataManager)
    {
        List<InstalledModuleEntity> entities = new();
        foreach (KeyValuePair<string, InventoryItem> itemEntry in equipment.equipment)
        {
            InventoryItem item = itemEntry.Value;
            if (item != null)
            {
                Pickupable pickupable = item.item;
                string classId = pickupable.RequireComponent<PrefabIdentifier>().ClassId;
                NitroxId itemId = NitroxEntity.GetIdOrGenerateNew(pickupable.gameObject);
                Optional<EntityMetadata> metadata = entityMetadataManager.Extract(pickupable.gameObject);
                List<Entity> children = GetPrefabChildren(pickupable.gameObject, itemId, entityMetadataManager).ToList();

                entities.Add(new(itemEntry.Key, classId, itemId, pickupable.GetTechType().ToDto(), metadata.OrNull(), equipmentId, children));
            }
        }
        return entities;
    }

    private static bool IsGlobalRootObject(GameObject gameObject)
    {
        return gameObject.TryGetComponent(out LargeWorldEntity largeWorldEntity) &&
            largeWorldEntity.initialCellLevel == LargeWorldEntity.CellLevel.Global;
    }
}
