using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic.Bases;
using NitroxClient.GameLogic.Spawning.Bases;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.Helpers;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Bases;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxPatcher.PatternMatching;
using UnityEngine;
using UWE;
using static System.Reflection.Emit.OpCodes;
using static NitroxClient.GameLogic.Bases.BuildingHandler;

namespace NitroxPatcher.Patches.Dynamic;

public sealed partial class Constructable_Construct_Patch : NitroxPatch, IDynamicPatch
{
    public static readonly MethodInfo TARGET_METHOD = Reflect.Method((Constructable t) => t.Construct());

    private static TemporaryBuildData Temp => BuildingHandler.Main.Temp;

    public static readonly InstructionsPattern InstructionsPattern = new()
    {
        Div,
        Stfld,
        Ldc_I4_0,
        Ret,
        Ldarg_0,
        { InstructionPattern.Call(nameof(Constructable), nameof(Constructable.UpdateMaterial)), "Insert" }
    };

    public static readonly List<CodeInstruction> InstructionsToAdd = new()
    {
        new(Ldarg_0),
        new(Ldc_I4_1), // True for "constructing"
        new(Call, Reflect.Method(() => ConstructionAmountModified(default, default)))
    };

    public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions) =>
        instructions.Transform(InstructionsPattern, (label, instruction) =>
        {
            if (label.Equals("Insert"))
            {
                return InstructionsToAdd;
            }
            return null;
        });

    public static void ConstructionAmountModified(Constructable constructable, bool constructing)
    {
        // We only manage the amount change, not the deconstruction/construction action
        if (!constructable.TryGetNitroxId(out NitroxId entityId))
        {
            Log.ErrorOnce($"[{nameof(ConstructionAmountModified)}] Couldn't find a NitroxEntity on {constructable.name}");
            return;
        }
        float amount = NitroxModel.Helper.Mathf.Clamp01(constructable.constructedAmount);

        // An object is destroyed when amount = 0 AND if we are destructing
        // so we don't need the broadcast if we are trying to construct with not enough resources (amount = 0)
        if (amount == 0f && constructing)
        {
            return;
        }

        /*
         * Different cases:
         * - Normal module (only Constructable), just let it go to 1.0f normally
         * - ConstructableBase:
         *   - if it's already in a base, simply update the current base
         *   - else, create a new base
         */
        if (amount == 1f)
        {
            Resolve<ThrottledPacketSender>().RemovePendingPackets(entityId);
            if (constructable is ConstructableBase constructableBase)
            {
                CoroutineHost.StartCoroutine(BroadcastObjectBuilt(constructableBase, entityId));
                return;
            }
            IEnumerator postSpawner = BuildingPostSpawner.ApplyPostSpawner(constructable.gameObject, entityId);
    
            // Can be null if no post spawner is set for the constructable's techtype
            if (postSpawner != null)
            {
                CoroutineHost.StartCoroutine(postSpawner);
            }
            // To avoid any unrequired throttled packet to be sent we clean the pending throttled packets for this object
            Resolve<IPacketSender>().Send(new ModifyConstructedAmount(entityId, 1f));
            return;
        }

        // update as a normal module
        Resolve<ThrottledPacketSender>().SendThrottled(new ModifyConstructedAmount(entityId, amount),
            (packet) => { return packet.GhostId; }, 0.1f);
    }

    public static IEnumerator BroadcastObjectBuilt(ConstructableBase constructableBase, NitroxId entityId)
    {
        BaseGhost baseGhost = constructableBase.model.GetComponent<BaseGhost>();
        constructableBase.SetState(true, true);
        if (!baseGhost.targetBase)
        {
            Log.Error("Something wrong happened, couldn't find base after finishing building ghost");
            yield break;
        }
        Base targetBase = baseGhost.targetBase;
        Base parentBase = null;
        if (constructableBase.tr.parent)
        {
            parentBase = constructableBase.GetComponentInParent<Base>(true);
        }

        // If a module was spawned we need to transfer the ghost id to it for further recognition
        BuildUtils.TryTransferIdFromGhostToModule(baseGhost, entityId, constructableBase, out GameObject moduleObject);

        // Have a delay for baseGhost to be actually destroyed
        yield return null;

        if (parentBase)
        {
            // update existing base
            if (!parentBase.TryGetNitroxId(out NitroxId parentId))
            {
                BuildingHandler.Main.FailedOperations++;
                Log.Error($"[{nameof(BroadcastObjectBuilt)}] Parent base doesn't have a NitroxEntity, which is not normal");
                yield break;
            }
            MoonpoolManager moonpoolManager = parentBase.gameObject.EnsureComponent<MoonpoolManager>();

            GlobalRootEntity builtPiece = null;
            if (moduleObject)
            {
                if (moduleObject.TryGetComponent(out IBaseModule builtModule))
                {
                    builtPiece = InteriorPieceEntitySpawner.From(builtModule, Resolve<EntityMetadataManager>());
                }
                else if (moduleObject.GetComponent<VehicleDockingBay>())
                {
                    builtPiece = moonpoolManager.LatestRegisteredMoonpool;
                }
                else if (moduleObject.TryGetComponent(out MapRoomFunctionality mapRoomFunctionality))
                {
                    builtPiece = BuildUtils.CreateMapRoomEntityFrom(mapRoomFunctionality, parentBase, entityId, parentId);
                }
            }

            SendUpdateBase(parentBase, parentId, entityId, builtPiece, moonpoolManager);
        }
        else
        {
            // Must happen before NitroxEntity.SetNewId because else, if a moonpool was marked with the same id, this id be will unlinked from the base object
            if (baseGhost.targetBase.TryGetComponent(out MoonpoolManager moonpoolManager))
            {
                moonpoolManager.LateAssignNitroxEntity(entityId);
                moonpoolManager.OnPostRebuildGeometry(baseGhost.targetBase);
            }
            // create a new base
            NitroxEntity.SetNewId(baseGhost.targetBase.gameObject, entityId);
            BuildingHandler.Main.EnsureTracker(entityId).ResetToId();

            Resolve<IPacketSender>().Send(new PlaceBase(entityId, BuildEntitySpawner.From(targetBase, Resolve<EntityMetadataManager>())));
        }

        if (moduleObject)
        {
            yield return BuildingPostSpawner.ApplyPostSpawner(moduleObject, entityId);
        }

        Temp.Dispose();
    }

    private static void SendUpdateBase(Base @base, NitroxId baseId, NitroxId pieceId, GlobalRootEntity builtPieceEntity, MoonpoolManager moonpoolManager)
    {
        List<Entity> childEntities = BuildUtils.GetChildEntities(@base, baseId, Resolve<EntityMetadataManager>());

        // We get InteriorPieceEntity children from the base and make up a dictionary with their updated data (their BaseFace)
        Dictionary<NitroxId, NitroxBaseFace> updatedChildren = childEntities.OfType<InteriorPieceEntity>()
            .ToDictionary(entity => entity.Id, entity => entity.BaseFace);
        // Same for MapRooms
        Dictionary<NitroxId, NitroxInt3> updatedMapRooms = childEntities.OfType<MapRoomEntity>()
            .ToDictionary(entity => entity.Id, entity => entity.Cell);

        BuildingHandler.Main.EnsureTracker(baseId).LocalOperations++;
        int operationId = BuildingHandler.Main.GetCurrentOperationIdOrDefault(baseId);

        UpdateBase updateBase = new(baseId, pieceId, BuildEntitySpawner.GetBaseData(@base), builtPieceEntity, updatedChildren, moonpoolManager.GetMoonpoolsUpdate(), updatedMapRooms, Temp.ChildrenTransfer, operationId);

        // TODO: (for server-side) Find a way to optimize this (maybe by copying BaseGhost.Finish() => Base.CopyFrom)
        Resolve<IPacketSender>().Send(updateBase);
    }
}
