using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core.Plugins;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Game.Rust.Cui;
using Pool = Facepunch.Pool;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Globalization;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Sort Button", "WhiteThunder, MON@H", "2.7.0")]
    [Description("Adds a sort button to storage boxes, allowing you to sort items by name or category")]
    internal class SortButton : CovalencePlugin
    {
        #region Fields

        private Configuration _config;

        [PluginReference]
        private readonly Plugin Clans, Friends;

        private const string PermissionUse = "sortbutton.use";
        private const string GUIPanelName = "UISortButton";

        private const int MaxRows = 8;
        private const float BaseYOffset = 113.5f;
        private const float YOffsetPerRow = 62;
        private const float SortButtonWidth = 79;
        private const float SortOrderButtonWidthString = 17;
        private const string ButtonHeightString = "23";

        // When calculating sort button position, do it based on the loot panel to simplify configuration.
        private readonly Dictionary<string, string> OffsetYByLootPanel = new()
        {
            ["dropboxcontents"] = (BaseYOffset + YOffsetPerRow * 2).ToString(),
            ["furnace"] = "277",
            ["generic"] = (BaseYOffset + YOffsetPerRow * 6).ToString(),
            ["genericsmall"] = (BaseYOffset + YOffsetPerRow).ToString(),
            ["largefurnace"] = "395",
            ["toolcupboard"] = "595",
            ["vendingmachine.storage"] = (BaseYOffset + YOffsetPerRow * 5).ToString(),
        };

        private readonly string[] OffsetYByRow = new string[MaxRows];

        // Since 2020/08, some loot panels still use 21px, while most other panels use 23px.
        private readonly Dictionary<string, string> HeightOverrideByLootPanel = new()
        {
            ["animal-storage"] = "21",
            ["dropboxcontents"] = "21",
            ["furnace"] = "21",
            ["largefurnace"] = "21",
            ["toolcupboard"] = "21.5",
            ["vendingmachine.storage"] = "21",
        };

        // When players sort by category, use numeric sort order for faster comparisons.
        private int[] _itemCategoryToSortIndex;

        // When players do not have data, use this shared object to avoid unnecessary heap allocations.
        private PlayerData _defaultPlayerData;

        // Cache the UI JSON to improve performance.
        private string _cachedUI;

        // Parameterize the cached UI JSON with the arguments stored in this array.
        private readonly string[] _uiArguments = new string[6];

        // Keep track of UI viewers to reduce unnecessary calls to destroy the UI.
        private readonly HashSet<ulong> _uiViewers = new();

        #endregion Fields

        #region OxideHooks

        private void Init()
        {
            UnsubscribeFromHooks();
            RegisterPermissions();
            AddCommands();
            LoadData();
            SetupItemCategories();

            _defaultPlayerData = new PlayerData
            {
                Enabled = _config.DefaultEnabled,
                SortByCategory = _config.DefaultSortByCategory,
            };

            for (var i = 0; i < MaxRows; i++)
            {
                OffsetYByRow[i] = (BaseYOffset + YOffsetPerRow * (i + 1)).ToString();
            }
        }

        private void OnServerInitialized()
        {
            _config.OnServerInitialized(this);

            SubscribeToHooks();
        }

        private void Unload()
        {
            foreach (var activePlayer in BasePlayer.activePlayerList)
            {
                DestroyUi(activePlayer);
            }
        }

        private void OnLootEntity(BasePlayer basePlayer, BaseEntity entity)
        {
            HandleOnLootEntity(basePlayer, entity, delay: true);
        }

        private void OnPlayerLootEnd(PlayerLoot inventory)
        {
            var player = inventory.baseEntity;
            if (player != null)
            {
                DestroyUi(player);
            }
        }

        // Only using this hook because some plugins don't call OnPlayerLootEnd.
        private void OnLootEntityEnd(BasePlayer player, BaseCombatEntity entity)
        {
            DestroyUi(player);
        }

        #endregion OxideHooks

        #region Commands

        private void CmdSortButton(IPlayer player, string cmd, string[] args)
        {
            if (player.IsServer)
                return;

            var basePlayer = player.Object as BasePlayer;

            if (!permission.UserHasPermission(basePlayer.UserIDString, PermissionUse))
            {
                PlayerSendMessage(basePlayer, Lang(LangKeys.Error.NoPermission, basePlayer.UserIDString));
                return;
            }

            var playerData = GetPlayerData(basePlayer.userID, createIfMissing: true);

            if (args == null || args.Length == 0)
            {
                playerData.Enabled = !playerData.Enabled;
                SaveData();

                var enabledOrDisabledMessage = playerData.Enabled
                    ? Lang(LangKeys.Format.Enabled, basePlayer.UserIDString)
                    : Lang(LangKeys.Format.Disabled, basePlayer.UserIDString);

                PlayerSendMessage(basePlayer, Lang(LangKeys.Info.ButtonStatus, basePlayer.UserIDString, enabledOrDisabledMessage));
                return;
            }

            switch (args[0].ToLower())
            {
                case "sort":
                case "type":
                    playerData.SortByCategory = !playerData.SortByCategory;
                    SaveData();

                    var sortTypeLangKey = playerData.SortByCategory
                        ? LangKeys.Format.Category
                        : LangKeys.Format.Name;

                    PlayerSendMessage(basePlayer, Lang(LangKeys.Info.SortType, basePlayer.UserIDString, Lang(sortTypeLangKey, basePlayer.UserIDString)));
                    return;
            }

            PlayerSendMessage(basePlayer, Lang(LangKeys.Info.Help, basePlayer.UserIDString, _config.Commands[0]));
        }

        [Command("sortbutton.order")]
        private void Command_SortType(IPlayer player)
        {
            if (player.IsServer || !player.HasPermission(PermissionUse))
                return;

            var basePlayer = player.Object as BasePlayer;
            var playerData = GetPlayerData(basePlayer.userID, createIfMissing: true);

            playerData.SortByCategory = !playerData.SortByCategory;
            SaveData();

            RecreateSortButton(basePlayer);
        }

        [Command("sortbutton.sort")]
        private void Command_Sort(IPlayer player)
        {
            if (player.IsServer || !player.HasPermission(PermissionUse))
                return;

            var basePlayer = player.Object as BasePlayer;
            var containers = basePlayer.inventory.loot.containers;

            // Sorting loot panels with multiple containers is not supported at this time.
            if (containers.Count != 1)
                return;

            var entitySource = basePlayer.inventory.loot.entitySource;

            // Verify the container is supported.
            var containerConfiguration = _config.GetContainerConfiguration(entitySource);
            if (containerConfiguration is not { Enabled: true })
                return;

            // Verify entity-specific checks like for drop boxes and vending machines.
            if (!CanPlayerSortEntity(basePlayer, entitySource))
                return;

            // Verify the player hasn't disabled the sort button.
            var playerData = GetPlayerData(basePlayer.userID);
            if (!playerData.Enabled)
                return;

            // If the container is owned by another player, verify the looter is authorized to sort.
            var ownerID = entitySource.OwnerID;
            if (_config.CheckOwnership && ownerID != 0 && !IsAlly(basePlayer.userID, ownerID))
                return;

            foreach (var container in basePlayer.inventory.loot.containers)
            {
                if (!IsSortableContainer(container))
                    continue;

                SortContainer(container, basePlayer, playerData.SortByCategory);
            }
        }

        #endregion Commands

        #region Helpers

        private void UnsubscribeFromHooks()
        {
            Unsubscribe(nameof(OnLootEntity));
            Unsubscribe(nameof(OnPlayerLootEnd));
        }

        private void SubscribeToHooks()
        {
            Subscribe(nameof(OnLootEntity));
            Subscribe(nameof(OnPlayerLootEnd));
        }

        private void SetupItemCategories()
        {
            var itemCategories = Enum.GetValues(typeof(ItemCategory)).Cast<ItemCategory>().ToList();
            itemCategories.Sort((a, b) => a.ToString().CompareTo(b.ToString()));

            _itemCategoryToSortIndex = new int[itemCategories.Count];

            for (var i = 0; i < itemCategories.Count; i++)
            {
                var itemCategory = itemCategories[i];
                _itemCategoryToSortIndex[(int)itemCategory] = i;
            }
        }

        private void RegisterPermissions()
        {
            permission.RegisterPermission(PermissionUse, this);
        }

        private void AddCommands()
        {
            if (_config.Commands.Count == 0)
            {
                _config.Commands = new List<string>() { "sortbutton" };
                SaveConfig();
            }

            foreach (var command in _config.Commands)
            {
                AddCovalenceCommand(command, nameof(CmdSortButton));
            }
        }

        private bool IsPluginLoaded(Plugin plugin) => plugin is { IsLoaded: true };

        private bool IsAlly(ulong playerId, ulong targetId)
        {
            if (playerId == targetId || IsOnSameTeam(playerId, targetId))
                return true;

            var playerIdString = playerId.ToString();
            var targetIdString = targetId.ToString();

            return IsClanMemberOrAlly(playerIdString, targetIdString)
                || IsFriend(playerIdString, targetIdString);
        }

        private bool IsClanMemberOrAlly(string playerId, string targetId)
        {
            if (_config.UseClans)
            {
                if (IsPluginLoaded(Clans))
                    return Clans.Call<bool>("IsMemberOrAlly", playerId, targetId);

                PrintError("UseClans is set to true, but plugin Clans is not loaded!");
            }

            return false;
        }

        private bool IsFriend(string playerId, string targetId)
        {
            if (_config.UseFriends)
            {
                if (IsPluginLoaded(Friends))
                    return Friends.Call<bool>("HasFriend", targetId, playerId);

                PrintError("UseFriends is set to true, but plugin Friends is not loaded!");
            }

            return false;
        }

        private bool IsOnSameTeam(ulong playerId, ulong targetId)
        {
            if (!_config.UseTeams)
                return false;

            var playerTeam = RelationshipManager.ServerInstance.FindPlayersTeam(playerId);
            return playerTeam?.members.Contains(targetId) ?? false;
        }

        private void PlayerSendMessage(BasePlayer player, string message)
        {
            message = Lang(LangKeys.Format.Prefix, player.UserIDString) + message;
            player.SendConsoleCommand("chat.add", 2, _config.SteamIDIcon, message);
        }

        #endregion Helpers

        #region Core Methods

        private static T GetChildEntity<T>(BaseEntity entity) where T : BaseEntity
        {
            foreach (var child in entity.children)
            {
                var childOfType = child as T;
                if (childOfType != null)
                    return childOfType;
            }

            return null;
        }

        private static bool HasIndustrialAdaptor(BaseEntity entity, out IndustrialStorageAdaptor adaptor)
        {
            adaptor = null;

            if (entity is not StorageContainer storageContainer)
                return false;

            if (!storageContainer.allowSorting)
                return false;

            adaptor = GetChildEntity<IndustrialStorageAdaptor>(storageContainer);
            return adaptor != null;
        }

        private void HandleOnLootEntityDelayed(BasePlayer basePlayer, BaseEntity entity, string offsetXString, bool sortByCategory)
        {
            // Sorting loot panels with multiple containers is not supported at this time.
            if (basePlayer.inventory.loot.containers.Count != 1)
                return;

            var container = basePlayer.inventory.loot.containers.FirstOrDefault();

            // Don't show the sort button for the ridable horse equipment inventory.
            if ((entity is RidableHorse horse && container != horse.storageInventory))
                return;

            var lootPanelName = DetermineLootPanelName(entity);
            if (!TryDetermineYOffset(container, lootPanelName, out var offsetYString))
                return;

            if (!HeightOverrideByLootPanel.TryGetValue(lootPanelName, out var heightString))
            {
                heightString = ButtonHeightString;
            }

            CreateButtonUI(basePlayer, offsetXString, offsetYString, heightString, sortByCategory);
        }

        private void HandleOnLootEntity(BasePlayer basePlayer, BaseEntity entity, bool delay = true)
        {
            if (basePlayer == null
                || !permission.UserHasPermission(basePlayer.UserIDString, PermissionUse))
                return;

            // Verify the container is supported.
            var containerConfiguration = _config.GetContainerConfiguration(entity);
            if (containerConfiguration == null || !containerConfiguration.Enabled)
                return;

            // Verify entity-specific checks like for drop boxes and vending machines.
            if (!CanPlayerSortEntity(basePlayer, entity))
                return;

            // Verify the player hasn't disabled the sort button.
            var playerData = GetPlayerData(basePlayer.userID);
            if (!playerData.Enabled)
                return;

            // If the container is owned by another player, verify the looter is authorized to sort.
            var ownerID = entity.OwnerID;
            if (_config.CheckOwnership && ownerID != 0 && !IsAlly(basePlayer.userID, ownerID))
                return;

            // If industrial adaptor is attached and powered, do not show the sort button.
            // If not powered, show the sort button, but potentially with an offset position to avoid overlap.
            var industrialSortingEnabled = HasIndustrialAdaptor(entity, out var adaptor);
            if (industrialSortingEnabled && adaptor.IsPowered())
                return;

            var offsetXString = containerConfiguration.GetOffsetXString(industrialSortingEnabled);
            var sortByCategory = playerData.SortByCategory;

            if (delay)
            {
                // Delay showing the sort button, so that we can determine which containers the player is actually viewing.
                NextTick(() =>
                {
                    if (basePlayer == null
                        || basePlayer.IsDestroyed
                        || entity == null
                        || entity.IsDestroyed)
                        return;

                    HandleOnLootEntityDelayed(basePlayer, entity, offsetXString, sortByCategory);
                });
            }
            else
            {
                HandleOnLootEntityDelayed(basePlayer, entity, offsetXString, sortByCategory);
            }
        }

        private bool IsSortableContainer(ItemContainer container)
        {
            if (container.IsLocked()
                || container.PlayerItemInputBlocked()
                || container.HasFlag(ItemContainer.Flag.IsPlayer)
                || container.capacity <= 1)
                return false;

            return true;
        }

        private bool CanPlayerSortEntity(BasePlayer basePlayer, BaseEntity entity)
        {
            if (entity == null || entity.IsDestroyed)
                return false;

            if (entity is DropBox dropBox)
                return dropBox.PlayerBehind(basePlayer);

            if (entity is VendingMachine vendingMachine)
                return vendingMachine.PlayerBehind(basePlayer);

            return true;
        }

        private string DetermineLootPanelName(BaseEntity entity)
        {
            return entity switch
            {
                Mailbox mailbox => mailbox.ownerPanel,
                StorageContainer storageContainer => storageContainer.panelName,
                RidableHorse horse => horse.storagePanelName,
                _ => "generic_resizable",
            };
        }

        private bool TryDetermineYOffset(ItemContainer container, string lootPanelName, out string offsetYString)
        {
            if (lootPanelName is "generic_resizable" or "animal-storage")
            {
                var numRows = Math.Min(1 + (container.capacity - 1) / 6, MaxRows);
                offsetYString = OffsetYByRow[numRows - 1];
                return true;
            }

            if (OffsetYByLootPanel.TryGetValue(lootPanelName, out offsetYString))
                return true;

            return false;
        }

        private int CompareItems(Item a, Item b, bool byCategory = false)
        {
            if (byCategory)
            {
                var categoryIndex = _itemCategoryToSortIndex[(int)a.info.category];
                var otherCategoryIndex = _itemCategoryToSortIndex[(int)b.info.category];

                var categoryComparison = categoryIndex.CompareTo(otherCategoryIndex);
                if (categoryComparison != 0)
                    return categoryComparison;
            }

            var nameComparison = a.info.displayName.translated.CompareTo(b.info.displayName.translated);
            if (nameComparison != 0)
                return nameComparison;

            return a.amount.CompareTo(b.amount);
        }

        private void SortContainer(ItemContainer container, BasePlayer initiator, bool byCategory)
        {
            var itemList = Pool.Get<List<Item>>();

            if (container.entityOwner is BuildingPrivlidge)
            {
                for (var i = container.itemList.Count - 1; i >= 0; i--)
                {
                    var item = container.itemList[i];
                    if (item.position >= 24)
                        continue;

                    item.RemoveFromContainer();
                    itemList.Add(item);
                }
            }
            else
            {
                for (var i = container.itemList.Count - 1; i >= 0; i--)
                {
                    var item = container.itemList[i];
                    item.RemoveFromContainer();
                    itemList.Add(item);
                }
            }

            if (byCategory)
            {
                itemList.Sort((a, b) => CompareItems(a, b, byCategory: true));
            }
            else
            {
                itemList.Sort((a, b) => CompareItems(a, b, byCategory: false));
            }

            foreach (var item in itemList)
            {
                if (!item.MoveToContainer(container))
                {
                    initiator.GiveItem(item);
                }
            }

            Pool.FreeUnmanaged(ref itemList);
        }

        #endregion Core Methods

        #region GUI

        private void CreateButtonUI(BasePlayer player, string offsetXString, string offsetYString, string heightString, bool sortByCategory)
        {
            if (!_uiViewers.Add(player.userID))
                return;

            if (_cachedUI == null)
            {
                var elements = new CuiElementContainer();

                elements.Add(new CuiPanel
                {
                    Image =
                    {
                        Color = "0 0 0 0",
                    },
                    RectTransform =
                    {
                        AnchorMin = "0.5 0",
                        AnchorMax = "0.5 0",
                        OffsetMin = "{0} {1}",
                        OffsetMax = "{0} {1}",
                    },
                    CursorEnabled = false,
                }, "Overlay", GUIPanelName);

                elements.Add(new CuiButton
                {
                    Button =
                    {
                        Command = "sortbutton.order",
                        Color = "{2}",
                    },
                    RectTransform =
                    {
                        AnchorMin = "0 0",
                        AnchorMax = "0 0",
                        OffsetMin = "0 0",
                        OffsetMax = $"{SortOrderButtonWidthString} {{3}}",
                    },
                    Text =
                    {
                        Text = "{4}",
                        FontSize = 12,
                        Align = TextAnchor.MiddleCenter,
                        Color = "0.77 0.92 0.67 0.8",
                    },
                }, GUIPanelName);

                elements.Add(new CuiButton
                {
                    Button =
                    {
                        Command = "sortbutton.sort",
                        Color = "0.41 0.50 0.25 0.8",
                    },
                    RectTransform =
                    {
                        AnchorMin = "0 0",
                        AnchorMax = "0 0",
                        OffsetMin = $"{SortOrderButtonWidthString} 0",
                        OffsetMax = $"{SortOrderButtonWidthString + SortButtonWidth} {{3}}",
                    },
                    Text =
                    {
                        Text = "{5}",
                        FontSize = 12,
                        Align = TextAnchor.MiddleCenter,
                        Color = "0.77 0.92 0.67 0.8",
                    },
                }, GUIPanelName);

                _cachedUI = CuiHelper.ToJson(elements);

                // Escape braces for string.Format.
                _cachedUI = _cachedUI.Replace("{", "{{").Replace("}", "}}");

                for (var i = 0; i < _uiArguments.Length; i++)
                {
                    // Unescape braces for intended placeholders.
                    _cachedUI = _cachedUI.Replace("{{" + i + "}}", "{" + i + "}");
                }
            }

            _uiArguments[0] = offsetXString;
            _uiArguments[1] = offsetYString;

            // Order button color.
            _uiArguments[2] = sortByCategory ? "0.75 0.43 0.18 0.8" : "0.26 0.58 0.80 0.8";

            // Button height.
            _uiArguments[3] = heightString;

            // Order button text.
            _uiArguments[4] = sortByCategory ? "C" : "N";

            // Sort button text.
            _uiArguments[5] = lang.GetMessage(LangKeys.Format.ButtonText, this, player.UserIDString);

            CuiHelper.AddUi(player, string.Format(_cachedUI, _uiArguments));
        }

        private void RecreateSortButton(BasePlayer player)
        {
            DestroyUi(player);

            var storage = player.inventory.loot?.entitySource as StorageContainer;
            if ((object)storage != null)
            {
                HandleOnLootEntity(player, storage, delay: false);
            }
        }

        private void DestroyUi(BasePlayer player)
        {
            if (!_uiViewers.Remove(player.userID))
                return;

            CuiHelper.DestroyUi(player, GUIPanelName);
        }

        #endregion GUI

        #region DataFile

        private StoredData _storedData;

        private class StoredData
        {
            public readonly Hash<ulong, PlayerData> PlayerData = new();
        }

        private class PlayerData
        {
            public bool Enabled;
            public bool SortByCategory;
        }

        private PlayerData GetPlayerData(ulong userID, bool createIfMissing = false)
        {
            var playerData = _storedData.PlayerData[userID];
            if (playerData != null)
                return playerData;

            if (createIfMissing)
            {
                playerData = new PlayerData()
                {
                    Enabled = _config.DefaultEnabled,
                    SortByCategory = _config.DefaultSortByCategory,
                };

                _storedData.PlayerData[userID] = playerData;

                return playerData;
            }

            return _defaultPlayerData;
        }

        private void LoadData()
        {
            try
            {
                _storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
            }
            catch
            {
                ClearData();
            }
        }

        private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(Name, _storedData);

        private void ClearData()
        {
            _storedData = new StoredData();
            SaveData();
        }

        #endregion DataFile

        #region Configuration

        private class ContainerConfiguration
        {
            private const float DefaultOffsetX = 476.5f;
            private const float XOffsetForIndustrialAdapter = 49f;
            private const float MaxOffsetXForIndustrialAdapterAdjustment = DefaultOffsetX - XOffsetForIndustrialAdapter;

            [JsonProperty("Enabled")]
            public bool Enabled = true;

            [JsonProperty("OffsetX")]
            public float OffsetX = DefaultOffsetX;

            [JsonIgnore]
            private string _offsetXString;

            [JsonIgnore]
            private string _offsetXStringForIndustrialAdapter;

            public string GetOffsetXString(bool industrialSortingEnabled)
            {
                if (industrialSortingEnabled && OffsetX >= MaxOffsetXForIndustrialAdapterAdjustment)
                    return _offsetXStringForIndustrialAdapter ??= MaxOffsetXForIndustrialAdapterAdjustment.ToString(CultureInfo.InvariantCulture);

                return _offsetXString ??= OffsetX.ToString(CultureInfo.InvariantCulture);
            }
        }

        private class Configuration : BaseConfiguration
        {
            private static HashSet<string> OldRemovedPrefabs = new()
            {
                "assets/rust.ai/nextai/testridablehorse.prefab",
                "assets/content/vehicles/horse/ridablehorse2.prefab",
                "assets/content/vehicles/horse/_old/testridablehorse.prefab",
            };

            [JsonProperty("Default enabled")]
            public bool DefaultEnabled = true;

            [JsonProperty("Default sort by category")]
            public bool DefaultSortByCategory = true;

            [JsonProperty("Check ownership")]
            public bool CheckOwnership = true;

            [JsonProperty("Use Clans")]
            public bool UseClans = true;

            [JsonProperty("Use Friends")]
            public bool UseFriends = true;

            [JsonProperty("Use Teams")]
            public bool UseTeams = true;

            [JsonProperty("Chat steamID icon")]
            public ulong SteamIDIcon = 0;

            [JsonProperty("Chat command", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<string> Commands = new()
            {
                "sortbutton",
            };

            [JsonProperty("Containers by short prefab name")]
            private Dictionary<string, ContainerConfiguration> ContainersByPrefabPath = new Dictionary<string, ContainerConfiguration>
            {
                ["assets/content/vehicles/boats/rhib/subents/rhib_storage.prefab"] = new(),
                ["assets/content/vehicles/boats/rowboat/subents/rowboat_storage.prefab"] = new(),
                ["assets/content/vehicles/horse/ridablehorse.prefab"] = new(),
                ["assets/content/vehicles/modularcar/subents/modular_car_1mod_storage.prefab"] = new(),
                ["assets/content/vehicles/modularcar/subents/modular_car_camper_storage.prefab"] = new(),
                ["assets/content/vehicles/snowmobiles/subents/snowmobileitemstorage.prefab"] = new(),
                ["assets/content/vehicles/submarine/subents/submarineitemstorage.prefab"] = new(),
                ["assets/prefabs/deployable/composter/composter.prefab"] = new(),
                ["assets/prefabs/deployable/dropbox/dropbox.deployed.prefab"] = new(),
                ["assets/prefabs/deployable/fridge/fridge.deployed.prefab"] = new(),
                ["assets/prefabs/deployable/hitch & trough/hitchtrough.deployed.prefab"] = new(),
                ["assets/prefabs/deployable/hot air balloon/subents/hab_storage.prefab"] = new(),
                ["assets/prefabs/deployable/large wood storage/box.wooden.large.prefab"] = new(),
                ["assets/prefabs/deployable/large wood storage/skins/abyss_dlc_large_wood_box/abyss_dlc_storage_horizontal/abyss_barrel_horizontal.prefab"] = new(),
                ["assets/prefabs/deployable/large wood storage/skins/abyss_dlc_large_wood_box/abyss_dlc_storage_vertical/abyss_barrel_vertical.prefab"] = new(),
                ["assets/prefabs/deployable/large wood storage/skins/jungle_dlc_large_wood_box/jungle_dlc_storage_horizontal/wicker_barrel.prefab"] = new(),
                ["assets/prefabs/deployable/large wood storage/skins/jungle_dlc_large_wood_box/jungle_dlc_storage_vertical/bamboo_barrel.prefab"] = new(),
                ["assets/prefabs/deployable/large wood storage/skins/medieval_large_wood_box/medieval.box.wooden.large.prefab"] = new(),
                ["assets/prefabs/deployable/large wood storage/skins/warhammer_dlc_large_wood_box/krieg_storage_horizontal/krieg_storage_horizontal.prefab"] = new(),
                ["assets/prefabs/deployable/large wood storage/skins/warhammer_dlc_large_wood_box/krieg_storage_vertical/krieg_storage_vertical.prefab"] = new(),
                ["assets/prefabs/deployable/minifridge/minifridge.deployed.prefab"] = new(),
                ["assets/prefabs/deployable/small stash/small_stash_deployed.prefab"] = new(),
                ["assets/prefabs/deployable/tool cupboard/cupboard.tool.deployed.prefab"] = new(),
                ["assets/prefabs/deployable/tool cupboard/retro/cupboard.tool.retro.deployed.prefab"] = new(),
                ["assets/prefabs/deployable/tool cupboard/shockbyte/cupboard.tool.shockbyte.deployed.prefab"] = new(),
                ["assets/prefabs/deployable/vendingmachine/vendingmachine.deployed.prefab"] = new(),
                ["assets/prefabs/deployable/wall cabinet/electric.wallcabinet.deployed.prefab"] = new(),
                ["assets/prefabs/deployable/woodenbox/skins/pilot_hazmat_wooden_box/pilot_hazmat_woodbox_deployed.prefab"] = new(),
                ["assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab"] = new(),
                ["assets/prefabs/misc/decor_dlc/storagebarrel/storage_barrel_b.prefab"] = new(),
                ["assets/prefabs/misc/decor_dlc/storagebarrel/storage_barrel_c.prefab"] = new(),
                ["assets/prefabs/misc/halloween/coffin/coffinstorage.prefab"] = new(),
            };

            [JsonProperty("Containers by skin ID")]
            private Dictionary<ulong, ContainerConfiguration> ContainersBySkinId = new();

            [JsonIgnore]
            private Dictionary<uint, ContainerConfiguration> ContainersByPrefabId = new();

            public void OnServerInitialized(SortButton plugin)
            {
                List<string> addedPrefabs = null;
                List<string> prefabsToRemove = null;

                foreach (var prefabPath in DiscoverStoragePrefabs())
                {
                    if (ContainersByPrefabPath.ContainsKey(prefabPath))
                        continue;

                    ContainersByPrefabPath[prefabPath] = new ContainerConfiguration();

                    addedPrefabs ??= new List<string>();
                    addedPrefabs.Add(prefabPath);
                }

                foreach (var (prefabPath, containerConfig) in ContainersByPrefabPath)
                {
                    var baseEntity = GameManager.server.FindPrefab(prefabPath)?.GetComponent<BaseEntity>();
                    if (baseEntity == null)
                    {
                        if (OldRemovedPrefabs.Contains(prefabPath))
                        {
                            prefabsToRemove ??= new List<string>();
                            prefabsToRemove.Add(prefabPath);
                        }
                        else
                        {
                            plugin.LogError($"Invalid prefab in configuration: {prefabPath}");
                        }

                        continue;
                    }

                    ContainersByPrefabId[baseEntity.prefabID] = containerConfig;
                }

                if (prefabsToRemove?.Count > 0)
                {
                    foreach (var prefabPath in prefabsToRemove)
                    {
                        ContainersByPrefabPath.Remove(prefabPath);
                    }
                }

                if (!UsingDefaults && (prefabsToRemove?.Count > 0 || addedPrefabs?.Count > 0))
                {
                    plugin.SaveConfig();

                    if (addedPrefabs?.Count > 0)
                    {
                        plugin.LogWarning($"Discovered and added {addedPrefabs.Count} storage entity prefabs to Configuration.\n - {string.Join("\n - ", addedPrefabs)}");
                    }
                }
            }

            public ContainerConfiguration GetContainerConfiguration(BaseEntity entity)
            {
                if (entity.skinID != 0 && ContainersBySkinId.TryGetValue(entity.skinID, out var containerConfiguration))
                    return containerConfiguration;

                if (ContainersByPrefabId.TryGetValue(entity.prefabID, out containerConfiguration))
                    return containerConfiguration;

                return null;
            }

            private List<string> DiscoverStoragePrefabs()
            {
                var prefabList = new List<string>();

                foreach (var itemDefinition in ItemManager.itemList)
                {
                    var itemModDeployable = itemDefinition.GetComponent<ItemModDeployable>();
                    if (itemModDeployable == null)
                        continue;

                    var deployableEntity = itemModDeployable.entityPrefab.GetEntity();
                    if (deployableEntity is not (BoxStorage or BuildingPrivlidge or Fridge))
                        continue;

                    // Filter out `unused_storage_barrel_a` and potentially others like it.
                    if (deployableEntity.PrefabName.Contains("unused"))
                        continue;

                    prefabList.Add(deployableEntity.PrefabName);
                }

                return prefabList;
            }
        }

        #region Configuration Helpers

        private class BaseConfiguration
        {
            [JsonIgnore]
            public bool UsingDefaults;

            public string ToJson() => JsonConvert.SerializeObject(this);

            public Dictionary<string, object> ToDictionary() => JsonHelper.Deserialize(ToJson()) as Dictionary<string, object>;
        }

        private static class JsonHelper
        {
            public static object Deserialize(string json) => ToObject(JToken.Parse(json));

            private static object ToObject(JToken token)
            {
                switch (token.Type)
                {
                    case JTokenType.Object:
                        return token.Children<JProperty>()
                                    .ToDictionary(prop => prop.Name,
                                                  prop => ToObject(prop.Value));

                    case JTokenType.Array:
                        return token.Select(ToObject).ToList();

                    default:
                        return ((JValue)token).Value;
                }
            }
        }

        private bool MaybeUpdateConfig(BaseConfiguration config)
        {
            var currentWithDefaults = config.ToDictionary();
            var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value);
            return MaybeUpdateConfigSection(currentWithDefaults, currentRaw);
        }

        private bool MaybeUpdateConfigSection(Dictionary<string, object> currentWithDefaults, Dictionary<string, object> currentRaw)
        {
            var changed = false;

            foreach (var key in currentWithDefaults.Keys)
            {
                if (currentRaw.TryGetValue(key, out var currentRawValue))
                {
                    var currentDictValue = currentRawValue as Dictionary<string, object>;

                    if (currentWithDefaults[key] is Dictionary<string, object> defaultDictValue)
                    {
                        if (currentDictValue == null)
                        {
                            currentRaw[key] = currentWithDefaults[key];
                            changed = true;
                        }
                        else if (MaybeUpdateConfigSection(defaultDictValue, currentDictValue))
                        {
                            changed = true;
                        }
                    }
                }
                else
                {
                    currentRaw[key] = currentWithDefaults[key];
                    changed = true;
                }
            }

            return changed;
        }

        protected override void LoadDefaultConfig() => _config = new Configuration();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<Configuration>();
                if (_config == null)
                {
                    throw new JsonException();
                }

                if (MaybeUpdateConfig(_config))
                {
                    LogWarning("Configuration appears to be outdated; updating and saving");
                    SaveConfig();
                }
            }
            catch (Exception e)
            {
                LogError(e.Message);
                LogWarning($"Configuration file {Name}.json is invalid; using defaults");
                LoadDefaultConfig();
                _config.UsingDefaults = true;
            }
        }

        protected override void SaveConfig()
        {
            Log($"Configuration changes saved to {Name}.json");
            Config.WriteObject(_config, true);
        }

        #endregion Configuration Helpers

        #endregion Configuration

        #region Localization

        private string Lang(string key, string userIDString = null, params object[] args)
        {
            try
            {
                return string.Format(lang.GetMessage(key, this, userIDString), args);
            }
            catch (Exception ex)
            {
                PrintError($"Lang Key '{key}' threw exception:\n{ex}");
                throw;
            }
        }

        private static class LangKeys
        {
            public static class Error
            {
                private const string Base = nameof(Error) + ".";
                public const string NoPermission = Base + nameof(NoPermission);
            }

            public static class Info
            {
                private const string Base = nameof(Info) + ".";
                public const string ButtonStatus = Base + nameof(ButtonStatus);
                public const string Help = Base + nameof(Help);
                public const string SortType = Base + nameof(SortType);
            }

            public static class Format
            {
                private const string Base = nameof(Format) + ".";
                public const string ButtonText = Base + nameof(ButtonText);
                public const string Category = Base + nameof(Category);
                public const string Disabled = Base + nameof(Disabled);
                public const string Enabled = Base + nameof(Enabled);
                public const string Name = Base + nameof(Name);
                public const string Prefix = Base + nameof(Prefix);
            }
        }

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                [LangKeys.Error.NoPermission] = "You do not have permission to use this command",
                [LangKeys.Format.ButtonText] = "Sort",
                [LangKeys.Format.Category] = "<color=#D2691E>Category</color>",
                [LangKeys.Format.Disabled] = "<color=#B22222>Disabled</color>",
                [LangKeys.Format.Enabled] = "<color=#228B22>Enabled</color>",
                [LangKeys.Format.Name] = "<color=#00BFFF>Name</color>",
                [LangKeys.Format.Prefix] = "<color=#00FF00>[Sort Button]</color>: ",
                [LangKeys.Info.ButtonStatus] = "Sort Button is now {0}",
                [LangKeys.Info.SortType] = "Sort Type is now {0}",
                [LangKeys.Info.Help] = "List Commands:\n" +
                "<color=#FFFF00>/{0}</color> - Enable/Disable Sort Button.\n" +
                "<color=#FFFF00>/{0} <sort | type></color> - change sort type.",
            }, this);
            lang.RegisterMessages(new Dictionary<string, string>
            {
                [LangKeys.Error.NoPermission] = "У вас нет разрешения на использование этой команды",
                [LangKeys.Format.ButtonText] = "Сортировать",
                [LangKeys.Format.Category] = "<color=#D2691E>Категория</color>",
                [LangKeys.Format.Disabled] = "<color=#B22222>Отключена</color>",
                [LangKeys.Format.Enabled] = "<color=#228B22>Включена</color>",
                [LangKeys.Format.Name] = "<color=#00BFFF>Имя</color>",
                [LangKeys.Format.Prefix] = "<color=#00FF00>[Sort Button]</color>: ",
                [LangKeys.Info.ButtonStatus] = "Кнопка сортировки теперь {0}",
                [LangKeys.Info.SortType] = "Тип сортировки теперь {0}",
                [LangKeys.Info.Help] = "Список команд:\n" +
                "<color=#FFFF00>/{0}</color> - Включить/Отключить кнопку сортировки.\n" +
                "<color=#FFFF00>/{0} <sort | type></color> - изменить тип сортировки.",
            }, this, "ru");
        }

        #endregion Localization
    }
}
