using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Barotrauma.Networking;
using System.Collections;
using System.Collections.Immutable;
#if SERVER
using Barotrauma.Networking;
#endif

namespace Barotrauma
{
    class PurchasedItem
    {
        public ItemPrefab ItemPrefab => ItemPrefab.Prefabs[ItemPrefabIdentifier];
        public Identifier ItemPrefabIdentifier { get; }
        
        public int Quantity { get; set; }
        public bool? IsStoreComponentEnabled { get; set; }

        public readonly int BuyerCharacterInfoIdentifier;

        /// <summary>
        /// Should the items be given to the buyer immediately, as opposed to spawning them in the sub the next round?
        /// </summary>
        public bool DeliverImmediately { get; set; }

        public bool Delivered;

        public PurchasedItem(ItemPrefab itemPrefab, int quantity, int buyerCharacterInfoId)
        {
            ItemPrefabIdentifier = itemPrefab.Identifier;
            Quantity = quantity;
            IsStoreComponentEnabled = null;
            BuyerCharacterInfoIdentifier = buyerCharacterInfoId;
        }

#if CLIENT
        public PurchasedItem(ItemPrefab itemPrefab, int quantity)
            : this(itemPrefab, quantity, buyer: null) { }
#endif
        public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer)
            : this(itemPrefab.Identifier, quantity, buyer) { }
        
        public PurchasedItem(Identifier itemPrefabId, int quantity, Client buyer)
        {
            ItemPrefabIdentifier = itemPrefabId;
            Quantity = quantity;
            IsStoreComponentEnabled = null;
            BuyerCharacterInfoIdentifier = buyer?.Character?.Info?.GetIdentifier() ?? Character.Controlled?.Info?.GetIdentifier() ?? 0;
        }

        public override string ToString()
        {
            return $"{ItemPrefab.Name} ({Quantity})";
        }
    }

    class SoldItem
    {
        public ItemPrefab ItemPrefab { get; }
        public ushort ID { get; private set; }
        public bool Removed { get; set; }
        public byte SellerID { get; }
        public SellOrigin Origin { get;  }

        public enum SellOrigin
        {
            Character,
            Submarine
        }

        public SoldItem(ItemPrefab itemPrefab, ushort id, bool removed, byte sellerId, SellOrigin origin)
        {
            ItemPrefab = itemPrefab;
            ID = id;
            Removed = removed;
            SellerID = sellerId;
            Origin = origin;
        }

        public void SetItemId(ushort id)
        {
            if (ID != Entity.NullEntityID)
            {
                DebugConsole.LogError("Error setting SoldItem.ID: ID has already been set and should not be changed.");
                return;
            }
            ID = id;
        }
    }

    partial class CargoManager
    {
        private class SoldEntity
        {
            public enum SellStatus
            {
                /// <summary>
                /// Entity sold in SP. Or, entity sold by client and confirmed by server in MP.
                /// </summary>
                Confirmed,
                /// <summary>
                /// Entity sold by client in MP. Client has received at least one update from server after selling, but this entity wasn't yet confirmed.
                /// </summary>
                Unconfirmed,
                /// <summary>
                /// Entity sold by client in MP. Client hasn't yet received an update from server after selling.
                /// </summary>
                Local
            }

            public Item Item { get; private set; }
            public ItemPrefab ItemPrefab { get; }
            public SellStatus Status { get; set; }

            public SoldEntity(Item item, SellStatus status)
            {
                Item = item;
                ItemPrefab = item?.Prefab;
                Status = status;
            }

            public SoldEntity(ItemPrefab itemPrefab, SellStatus status)
            {
                ItemPrefab = itemPrefab;
                Status = status;
            }

            public void SetItem(Item item)
            {
                if (Item != null)
                {
                    DebugConsole.LogError($"Trying to set SoldEntity.Item, but it's already set!\n{Environment.StackTrace.CleanupStackTrace()}");
                    return;
                }
                Item = item;
            }
        }

        public const int MaxQuantity = 100;

        public Dictionary<Identifier, List<PurchasedItem>> ItemsInBuyCrate { get; } = new Dictionary<Identifier, List<PurchasedItem>>();
        public Dictionary<Identifier, List<PurchasedItem>> ItemsInSellCrate { get; } = new Dictionary<Identifier, List<PurchasedItem>>();
        public Dictionary<Identifier, List<PurchasedItem>> ItemsInSellFromSubCrate { get; } = new Dictionary<Identifier, List<PurchasedItem>>();
        public Dictionary<Identifier, List<PurchasedItem>> PurchasedItems { get; } = new Dictionary<Identifier, List<PurchasedItem>>();
        public Dictionary<Identifier, List<SoldItem>> SoldItems { get; } = new Dictionary<Identifier, List<SoldItem>>();

        private readonly CampaignMode campaign;

        private Location Location => campaign?.Map?.CurrentLocation;

        public readonly NamedEvent<CargoManager> OnItemsInBuyCrateChanged = new NamedEvent<CargoManager>();
        public readonly NamedEvent<CargoManager> OnItemsInSellCrateChanged = new NamedEvent<CargoManager>();
        public readonly NamedEvent<CargoManager> OnItemsInSellFromSubCrateChanged = new NamedEvent<CargoManager>();
        public readonly NamedEvent<CargoManager> OnPurchasedItemsChanged = new NamedEvent<CargoManager>();
        public readonly NamedEvent<CargoManager> OnSoldItemsChanged = new NamedEvent<CargoManager>();

        public CargoManager(CampaignMode campaign)
        {
            this.campaign = campaign;
        }

        public static bool HasUnlockedStoreItem(ItemPrefab prefab)
        {
            foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
            {
                if (character.HasStoreAccessForItem(prefab)) { return true; }
            }

            return false;
        }

        private List<T> GetItems<T>(Identifier identifier, Dictionary<Identifier, List<T>> items, bool create = false)
        {
            if (items.TryGetValue(identifier, out var storeSpecificItems) && storeSpecificItems != null)
            {
                return storeSpecificItems;
            }
            else if (create)
            {
                storeSpecificItems = new List<T>();
                items.Add(identifier, storeSpecificItems);
                return storeSpecificItems;
            }
            else
            {
                return new List<T>();
            }
        }

        public List<PurchasedItem> GetBuyCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInBuyCrate, create);

        public List<PurchasedItem> GetBuyCrateItems(Location.StoreInfo store, bool create = false) => GetBuyCrateItems(store?.Identifier ?? Identifier.Empty, create);

        public PurchasedItem GetBuyCrateItem(Identifier identifier, ItemPrefab prefab) => GetBuyCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab);

        public PurchasedItem GetBuyCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetBuyCrateItem(store?.Identifier ?? Identifier.Empty, prefab);

        public List<PurchasedItem> GetSellCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInSellCrate, create);

        public List<PurchasedItem> GetSellCrateItems(Location.StoreInfo store, bool create = false) => GetSellCrateItems(store?.Identifier ?? Identifier.Empty, create);

        public PurchasedItem GetSellCrateItem(Identifier identifier, ItemPrefab prefab) => GetSellCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab);

        public PurchasedItem GetSellCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetSellCrateItem(store?.Identifier ?? Identifier.Empty, prefab);

        public List<PurchasedItem> GetSubCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInSellFromSubCrate, create);

        public List<PurchasedItem> GetSubCrateItems(Location.StoreInfo store, bool create = false) => GetSubCrateItems(store?.Identifier ?? Identifier.Empty, create);

        public PurchasedItem GetSubCrateItem(Identifier identifier, ItemPrefab prefab) => GetSubCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab);

        public PurchasedItem GetSubCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetSubCrateItem(store?.Identifier ?? Identifier.Empty, prefab);

        public List<PurchasedItem> GetPurchasedItems(Identifier identifier, bool create = false) => GetItems(identifier, PurchasedItems, create);

        public List<PurchasedItem> GetPurchasedItems(Location.StoreInfo store, bool create = false) => GetPurchasedItems(store?.Identifier ?? Identifier.Empty, create);

        public int GetPurchasedItemCount(Location.StoreInfo store, ItemPrefab prefab) =>
            GetPurchasedItemCount(store?.Identifier ?? Identifier.Empty, prefab);

        public int GetPurchasedItemCount(Identifier identifier, ItemPrefab prefab) => 
            GetPurchasedItems(identifier)?.Where(i => i.ItemPrefab == prefab).Sum(it => it.Quantity) ?? 0;

        public List<SoldItem> GetSoldItems(Identifier identifier, bool create = false) => GetItems(identifier, SoldItems, create);

        public List<SoldItem> GetSoldItems(Location.StoreInfo store, bool create = false) => GetSoldItems(store?.Identifier ?? Identifier.Empty, create);

        public void ClearItemsInBuyCrate()
        {
            ItemsInBuyCrate.Clear();
            OnItemsInBuyCrateChanged?.Invoke(this);
        }

        public void ClearItemsInSellCrate()
        {
            ItemsInSellCrate.Clear();
            OnItemsInSellCrateChanged?.Invoke(this);
        }

        public void ClearItemsInSellFromSubCrate()
        {
            ItemsInSellFromSubCrate.Clear();
            OnItemsInSellFromSubCrateChanged?.Invoke(this);
        }

        public void SetPurchasedItems(Dictionary<Identifier, List<PurchasedItem>> purchasedItems)
        {
            if (purchasedItems.Count == 0 && PurchasedItems.Count == 0) { return; }
            PurchasedItems.Clear();
            foreach (var entry in purchasedItems)
            {
                PurchasedItems.Add(entry.Key, entry.Value);
            }
            OnPurchasedItemsChanged?.Invoke(this);
        }

        public void ModifyItemQuantityInBuyCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity, Client client = null)
        {
            if (GetBuyCrateItem(storeIdentifier, itemPrefab) is { } item)
            {
                item.Quantity += changeInQuantity;
                if (item.Quantity < 1)
                {
                    GetBuyCrateItems(storeIdentifier, create: true).Remove(item);
                }
            }
            else if (changeInQuantity > 0)
            {
                GetBuyCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity, client));
            }
            OnItemsInBuyCrateChanged?.Invoke(this);
        }

        public void ModifyItemQuantityInSubSellCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity, Client client = null)
        {
            if (GetSubCrateItem(storeIdentifier, itemPrefab) is { } item)
            {
                item.Quantity += changeInQuantity;
                if (item.Quantity < 1)
                {
                    GetSubCrateItems(storeIdentifier)?.Remove(item);
                }
            }
            else if (changeInQuantity > 0)
            {
                GetSubCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity, client));
            }
            OnItemsInSellFromSubCrateChanged?.Invoke(this);
        }

        public void PurchaseItems(Identifier storeIdentifier, List<PurchasedItem> itemsToPurchase, bool removeFromCrate, Client client = null)
        {
            var store = Location?.GetStore(storeIdentifier);
            if (store == null) { return; }
            var itemsPurchasedFromStore = GetPurchasedItems(storeIdentifier, create: true);
            // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction
            var buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, itemsToPurchase.Select(i => i.ItemPrefab));
            var itemsInStoreCrate = GetBuyCrateItems(storeIdentifier, create: true);
            //handle checking which items can be purchased and deducting money first
            foreach (PurchasedItem item in itemsToPurchase.ToList())
            {
                if (item.Quantity <= 0) { continue; }
                // Exchange money
                int itemValue = item.Quantity * buyValues[item.ItemPrefab];
                if (!campaign.TryPurchase(client, itemValue)) 
                {
                    itemsToPurchase.Remove(item);
                    continue;
                }

                // Add to the purchased items
                var purchasedItem = itemsPurchasedFromStore.Find(pi => pi.ItemPrefab == item.ItemPrefab && pi.DeliverImmediately == item.DeliverImmediately);
                if (purchasedItem != null)
                {
                    purchasedItem.Quantity += item.Quantity;
                }
                else
                {
                    purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client) { DeliverImmediately = item.DeliverImmediately };
                    itemsPurchasedFromStore.Add(purchasedItem);
                }
                purchasedItem.Delivered = item.DeliverImmediately;
                if (GameMain.IsSingleplayer)
                {
                    GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value);
                }
                store.Balance += itemValue;
            }
            //actually spawn the items at this point
            if (GameMain.NetworkMember is not { IsClient: true })
            {
                Character targetCharacter;
#if CLIENT
                targetCharacter = Character.Controlled;
                if (targetCharacter == null)
                {
                    DebugConsole.ThrowError("Failed to deliver items directly to a character (not controlling a character).");
                }
#else
                targetCharacter = client?.Character;
                if (targetCharacter == null)
                {
                    DebugConsole.ThrowError($"Failed to deliver items directly to a character ({(client == null ? "client was null" : $"client {client.Name} is not controlling a character")}).");
                }
#endif
                if (targetCharacter == null)
                {
                    DeliverItemsToSub(itemsToPurchase.Where(it => it.DeliverImmediately), Submarine.MainSub, this);
                }
                else
                {
                    DeliverItemsToCharacter(itemsToPurchase.Where(it => it.DeliverImmediately), targetCharacter, this);
                }

            }
            if (removeFromCrate)
            {
                foreach (PurchasedItem item in itemsToPurchase)
                {
                    // Remove from the shopping crate
                    if (itemsInStoreCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } crateItem)
                    {
                        crateItem.Quantity -= item.Quantity;
                        if (crateItem.Quantity < 1) { itemsInStoreCrate.Remove(crateItem); }
                    }
                }
            }
            OnPurchasedItemsChanged?.Invoke(this);
        }

        public Dictionary<ItemPrefab, int> GetBuyValuesAtCurrentLocation(Identifier storeIdentifier, IEnumerable<ItemPrefab> items)
        {
            var buyValues = new Dictionary<ItemPrefab, int>();
            var store = Location?.GetStore(storeIdentifier);
            if (store == null) { return buyValues; }
            foreach (var item in items)
            {
                if (item == null) { continue; }
                if (!buyValues.ContainsKey(item))
                {
                    int buyValue = store?.GetAdjustedItemBuyPrice(item) ?? 0;
                    buyValues.Add(item, buyValue);
                }
            }
            return buyValues;
        }

        public Dictionary<ItemPrefab, int> GetSellValuesAtCurrentLocation(Identifier storeIdentifier, IEnumerable<ItemPrefab> items)
        {
            var sellValues = new Dictionary<ItemPrefab, int>();
            var store = Location?.GetStore(storeIdentifier);
            if (store == null) { return sellValues; }
            foreach (var item in items)
            {
                if (item == null) { continue; }
                if (!sellValues.ContainsKey(item))
                {
                    int sellValue = store?.GetAdjustedItemSellPrice(item) ?? 0;
                    sellValues.Add(item, sellValue);
                }
            }
            return sellValues;
        }

        public void CreatePurchasedItems()
        {
            purchasedIDCards.Clear();
            var items = new List<PurchasedItem>();
            foreach (var storeSpecificItems in PurchasedItems)
            {
                items.AddRange(storeSpecificItems.Value.Where(it => !it.DeliverImmediately));
            }
            DeliverItemsToSub(items, Submarine.MainSub, this);
            PurchasedItems.Clear();
            OnPurchasedItemsChanged?.Invoke(this);
        }

        private Dictionary<ItemPrefab, int> UndeterminedSoldEntities { get; } = new Dictionary<ItemPrefab, int>();

        public IEnumerable<Item> GetSellableItemsFromSub()
        {
            if (Submarine.MainSub == null) { return new List<Item>(); }
            var confirmedSoldEntities = Enumerable.Empty<SoldEntity>();
            UndeterminedSoldEntities.Clear();
#if CLIENT
            confirmedSoldEntities = GetConfirmedSoldEntities();
            foreach (var soldEntity in SoldEntities)
            {
                if (soldEntity.Item != null) { continue; }
                if (UndeterminedSoldEntities.TryGetValue(soldEntity.ItemPrefab, out int count))
                {
                    UndeterminedSoldEntities[soldEntity.ItemPrefab] = count + 1;
                }
                else
                {
                    UndeterminedSoldEntities.Add(soldEntity.ItemPrefab, 1);
                }
            }
#endif
            return FindAllSellableItems().Where(it => IsItemSellable(it, confirmedSoldEntities)).ToList();
        }

        public static IReadOnlyCollection<Item> FindAllItemsOnPlayerAndSub(Character character)
        {
            List<Item> allItems = new();
            if (character?.Inventory is { } inv)
            {
               allItems.AddRange(inv.FindAllItems(recursive: true));
            }
            allItems.AddRange(FindAllSellableItems());
            return allItems;
        }

        public static IEnumerable<Item> FindAllSellableItems()
        {
            if (Submarine.MainSub is null) { return Enumerable.Empty<Item>(); }

            return Submarine.MainSub.GetItems(true).FindAll(static item =>
            {
                if (item.GetRootInventoryOwner() is Character) { return false; }
                if (!item.Components.All(static c => c is not Holdable { Attachable: true, Attached: true })) { return false; }
                if (!item.Components.All(static c => c is not Wire w || w.Connections.All(static c => c is null))) { return false; }
                if (!ItemAndAllContainersInteractable(item)) { return false; }
                if (!AllContainersAllowSellingItems(item)) { return false; }
                return true;
            }).Distinct();

            static bool AllContainersAllowSellingItems(Item item)
            {
                do
                {
                    item = item.Container;
                    if (item is null) { return true; }
                    if (item.HasTag(Tags.DontSellItems)) { return false; }
                    if (item.Components.Any(static c => c.DisallowSellingItemsFromContainer)) { return false; }
                } while (item != null);
                return true;
            }

            static bool ItemAndAllContainersInteractable(Item item)
            {
                do
                {
                    if (!item.IsPlayerTeamInteractable) { return false; }
                    item = item.Container;
                } while (item != null);
                return true;
            }
        }

        private bool IsItemSellable(Item item, IEnumerable<SoldEntity> confirmedItems)
        {
            if (item.Removed) { return false; }
            if (!item.Prefab.CanBeSold) { return false; }
            if (item.SpawnedInCurrentOutpost) { return false; }
            if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; }
            if (confirmedItems != null && confirmedItems.Any(ci => ci.Item == item)) { return false; }
            if (UndeterminedSoldEntities.TryGetValue(item.Prefab, out int count))
            {
                int newCount = count - 1;
                if (newCount > 0)
                {
                    UndeterminedSoldEntities[item.Prefab] = newCount;
                }
                else
                {
                    UndeterminedSoldEntities.Remove(item.Prefab);
                }
                return false;
            }
            //can't sell items in hidden inventories
            Item rootContainer = item.Container;
            while (rootContainer != null)
            {
                if (rootContainer.OwnInventory?.Container is { } containerComponent)
                {
                    if (!containerComponent.DrawInventory) { return false; }
                    if (!containerComponent.IsAccessible()) { return false; }
                }
                rootContainer = rootContainer.Container;
            }
            if (item.OwnInventory?.Container is ItemContainer itemContainer)
            {
                var containedItems = item.ContainedItems;
                if (containedItems.None()) { return true; }
                // Allow selling the item if contained items are unsellable and set to be removed on deconstruct
                if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; }
                if (confirmedItems != null)
                {
                    // Otherwise there must be no contained items or the contained items must be confirmed as sold
                    if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; }
                }
            }
            return true;
        }

        public static IEnumerable<Hull> FindCargoRooms(IEnumerable<Submarine> subs) => subs.SelectMany(s => FindCargoRooms(s));

        public static IEnumerable<Hull> FindCargoRooms(Submarine sub) => WayPoint.WayPointList
            .Where(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Cargo)
            .Select(wp => wp.CurrentHull)
            .Distinct();

        public static IEnumerable<Item> FilterCargoCrates(IEnumerable<Item> items, Func<Item, bool> conditional = null)
            => items.Where(it => it.HasTag(Tags.Crate) && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.IsHidden && !it.Removed && (conditional == null || conditional(it)));

        public static IEnumerable<ItemContainer> FindReusableCargoContainers(IEnumerable<Submarine> subs, IEnumerable<Hull> cargoRooms = null) =>
            FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && !it.HasTag(Tags.CargoMissionItem) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull)))
                .Select(it => it.GetComponent<ItemContainer>())
                .Where(c => c != null);

        public static ItemContainer GetOrCreateCargoContainerFor(ItemPrefab item, ISpatialEntity cargoRoomOrSpawnPoint, ref List<ItemContainer> availableContainers)
        {
            ItemContainer itemContainer = null;
            if (!string.IsNullOrEmpty(item.CargoContainerIdentifier))
            {
                itemContainer = availableContainers.Find(ac =>
                    ac.Inventory.CanProbablyBePut(item) &&
                    (ac.Item.Prefab.Identifier == item.CargoContainerIdentifier ||
                    ac.Item.Prefab.Tags.Contains(item.CargoContainerIdentifier)));

                if (itemContainer == null)
                {
                    ItemPrefab containerPrefab = ItemPrefab.Prefabs.Find(ep =>
                        ep.Identifier == item.CargoContainerIdentifier ||
                        (ep.Tags != null && ep.Tags.Contains(item.CargoContainerIdentifier)));

                    if (containerPrefab == null)
                    {
                        DebugConsole.AddWarning($"CargoManager: could not find the item prefab for container {item.CargoContainerIdentifier}!");
                        return null;
                    }

                    Vector2 containerPosition = cargoRoomOrSpawnPoint is Hull cargoRoom ?  GetCargoPos(cargoRoom, containerPrefab) : cargoRoomOrSpawnPoint.Position;
                    Item containerItem = new Item(containerPrefab, containerPosition, cargoRoomOrSpawnPoint.Submarine);
                    itemContainer = containerItem.GetComponent<ItemContainer>();
                    if (itemContainer == null)
                    {
                        DebugConsole.AddWarning($"CargoManager: No ItemContainer component found in {containerItem.Prefab.Identifier}!");
                        return null;
                    }
                    if (!itemContainer.CanBeContained(item))
                    {
                        // Can't contain the item in the crate -> let's not create it.
                        containerItem.Remove();
                        return null;
                    }
                    availableContainers.Add(itemContainer);
#if SERVER
                    if (GameMain.Server != null)
                    {
                        Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item));
                    }
#endif
                }
            }
            return itemContainer;
        }

        public static void DeliverItemsToSub(IEnumerable<PurchasedItem> itemsToSpawn, Submarine sub, CargoManager cargoManager, bool showNotification = true)
        {
            if (!itemsToSpawn.Any()) { return; }

            WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub);
            if (wp == null)
            {
                DebugConsole.ThrowError("The submarine must have a waypoint marked as Cargo for bought items to be placed correctly!");
                return;
            }

            Hull cargoRoom = Hull.FindHull(wp.WorldPosition);
            if (cargoRoom == null)
            {
                DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!");
                return;
            }

            if (sub == Submarine.MainSub && itemsToSpawn.Any(it => !it.Delivered && it.Quantity > 0) && showNotification)
            {
#if CLIENT
                new GUIMessageBox("",
                    TextManager.GetWithVariable("CargoSpawnNotification",
                        "[roomname]",
                        cargoRoom.DisplayName,
                        FormatCapitals.Yes),
                    Array.Empty<LocalizedString>(),
                    type: GUIMessageBox.Type.InGame,
                    iconStyle: "StoreShoppingCrateIcon");
#else
                foreach (Client client in GameMain.Server.ConnectedClients)
                {
                    if (client.TeamID == CharacterTeamType.None || client.TeamID == sub.TeamID)
                    {
                        ChatMessage msg = ChatMessage.Create("",
                           TextManager.ContainsTag(cargoRoom.RoomName) ? $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}" : $"CargoSpawnNotification~[roomname]={cargoRoom.RoomName}", 
                           ChatMessageType.ServerMessageBoxInGame, null);
                        msg.IconStyle = "StoreShoppingCrateIcon";
                        GameMain.Server.SendDirectChatMessage(msg, client);
                    }
                }
#endif
            }
            var connectedSubs = sub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player);
            List<ItemContainer> availableContainers = FindReusableCargoContainers(connectedSubs, FindCargoRooms(connectedSubs)).ToList();
            foreach (PurchasedItem pi in itemsToSpawn)
            {
                pi.Delivered = true;
                Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab);
                for (int i = 0; i < pi.Quantity; i++)
                {
                    var item = new Item(pi.ItemPrefab, position, wp.Submarine);
                    var itemContainer = GetOrCreateCargoContainerFor(pi.ItemPrefab, cargoRoom, ref availableContainers);
                    itemContainer?.Inventory.TryPutItem(item, user: null);
                    ItemSpawned(pi, item, cargoManager);
#if SERVER
                    Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
                    (itemContainer?.Item ?? item).AssignCampaignInteractionType(CampaignMode.InteractionType.Cargo);
                }
            }
        }

        public static void DeliverItemsToCharacter(IEnumerable<PurchasedItem> itemsToSpawn, Character character, CargoManager cargoManager)
        {
            if (!itemsToSpawn.Any()) { return; }

            foreach (PurchasedItem pi in itemsToSpawn)
            {
                pi.Delivered = true;
                for (int i = 0; i < pi.Quantity; i++)
                {
                    var item = new Item(pi.ItemPrefab, character.Position, character.Submarine);
#if SERVER
                    Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
                    if (item.GetComponent<Holdable>() is { Attached: true })
                    {
                        item.Drop(dropper: null);
                    }
                    if (!character.Inventory.TryPutItem(item, user: null, item.AllowedSlots))
                    {
                        foreach (Item containedItem in character.Inventory.AllItemsMod)
                        {
                            //only put into containers that draw the inventory (not ones with a hidden inventory like circuit boxes!)
                            if (containedItem.OwnInventory?.Container is { DrawInventory: true } container && container.IsAccessible() &&
                                containedItem.OwnInventory.TryPutItem(item, user: null, item.AllowedSlots)) 
                            { 
                                break; 
                            }
                        }
                    }
                    ItemSpawned(pi, item, cargoManager);
                }
            }
        }
        private static void ItemSpawned(PurchasedItem purchased, Item item, CargoManager cargoManager)
        {
            var idCard = item.GetComponent<IdCard>();
            if (cargoManager != null && idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
            {
                if (purchased.DeliverImmediately)
                {
                    InitPurchasedIDCard(purchased, idCard);
                }
                else
                {
                    cargoManager.purchasedIDCards.Add((purchased, idCard));
                }
            }

            ItemSpawned(item);
        }

        public static void ItemSpawned(Item item)
        {
            CharacterTeamType teamID = CharacterTeamType.Team1;
            if (item.ParentInventory?.Owner is Character character)
            {
                teamID = character.TeamID;
            }
            else
            {
                Submarine sub = item.Submarine ?? item.RootContainer?.Submarine;
                if (sub != null)
                {
                    teamID = sub.TeamID;
                }
            }
            foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
            {
                wifiComponent.TeamID = teamID;
            }            
        }

        private readonly List<(PurchasedItem purchaseInfo, IdCard idCard)> purchasedIDCards = new List<(PurchasedItem purchaseInfo, IdCard idCard)>();
        public void InitPurchasedIDCards()
        {
            foreach ((PurchasedItem purchased, IdCard idCard) in purchasedIDCards)
            {
                InitPurchasedIDCard(purchased, idCard);
            }
        }

        private static void InitPurchasedIDCard(PurchasedItem purchased, IdCard idCard)
        {
            if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
            {
                var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier);
                if (owner?.Info != null)
                {
                    var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List<CharacterInfo>() { owner.Info }, Submarine.MainSub);
                    idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner);
                }
            }            
        }

        public static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab)
        {
            float floorPos = hull.Rect.Y - hull.Rect.Height;

            Vector2 position = new Vector2(
                hull.Rect.Width > 40 ? Rand.Range(hull.Rect.X + 20f, hull.Rect.Right - 20f) : hull.Rect.Center.X,
                floorPos);

            //check where the actual floor structure is in case the bottom of the hull extends below it
            if (Submarine.PickBody(
                ConvertUnits.ToSimUnits(new Vector2(position.X, hull.Rect.Y - hull.Rect.Height / 2)),
                ConvertUnits.ToSimUnits(position),
                collisionCategory: Physics.CollisionWall) != null)
            {
                float floorStructurePos = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition.Y);
                if (floorStructurePos > floorPos)
                {
                    floorPos = floorStructurePos;
                }
            }

            position.Y = floorPos + itemPrefab.Size.Y / 2;

            return position;
        }

        public void SavePurchasedItems(XElement parentElement)
        {
            var itemsElement = new XElement("cargo");
            foreach (var storeSpecificItems in PurchasedItems)
            {
                foreach (var item in storeSpecificItems.Value)
                {
                    if (item?.ItemPrefab == null) { continue; }
                    itemsElement.Add(new XElement("item",
                        new XAttribute("id", item.ItemPrefab.Identifier),
                        new XAttribute("qty", item.Quantity),
                        new XAttribute("storeid", storeSpecificItems.Key),
                        new XAttribute("deliverimmediately", item.DeliverImmediately),
                        new XAttribute("buyer", item.BuyerCharacterInfoIdentifier)));
                }
            }
            parentElement.Add(itemsElement);
        }

        public void LoadPurchasedItems(XElement element)
        {
            var purchasedItems = new Dictionary<Identifier, List<PurchasedItem>>();
            if (element != null)
            {
                foreach (XElement itemElement in element.GetChildElements("item"))
                {
                    string prefabId = itemElement.GetAttributeString("id", null);
                    if (string.IsNullOrWhiteSpace(prefabId)) { continue; }
                    if (!ItemPrefab.Prefabs.TryGet(prefabId.ToIdentifier(), out var prefab)) { continue; }
                    int qty = itemElement.GetAttributeInt("qty", 0);
                    Identifier storeId = itemElement.GetAttributeIdentifier("storeid", "merchant");
                    bool deliverImmediately = itemElement.GetAttributeBool("deliverimmediately", false);
                    int buyerId = itemElement.GetAttributeInt("buyer", 0);
                    if (!purchasedItems.TryGetValue(storeId, out var storeItems))
                    {
                        storeItems = new List<PurchasedItem>();
                        purchasedItems.Add(storeId, storeItems);
                    }
                    storeItems.Add(new PurchasedItem(prefab, qty, buyerId)
                    {
                        DeliverImmediately = deliverImmediately,
                        //must have already been delivered if we had opted for immediate delivery
                        Delivered = deliverImmediately
                    });
                }
            }
            SetPurchasedItems(purchasedItems);
        }
    }
}
