using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Better Researching", "Arainrr", "1.1.5")]
    [Description("Modify research time, cost, chance")]
    public class BetterResearching : RustPlugin
    {
        #region Fields

        [PluginReference] private readonly Plugin PopupNotifications, RustTranslationAPI;

        private static BetterResearching instance;
        private const string PREFAB_RESEARCH_TABLE = "assets/prefabs/deployable/research table/researchtable_deployed.prefab";

        private ItemDefinition researchResource;
        private ItemDefinition defaultResearchResource;
        private readonly Hash<ResearchTable, BasePlayer> researchers = new Hash<ResearchTable, BasePlayer>();
        private readonly Hash<ResearchTable, HashSet<BasePlayer>> lootingPlayers = new Hash<ResearchTable, HashSet<BasePlayer>>();

        #endregion Fields

        #region Oxide Hooks

        private void Init()
        {
            LoadData();
            instance = this;
            Unsubscribe(nameof(OnItemSplit));
            Unsubscribe(nameof(CanAcceptItem));
            Unsubscribe(nameof(OnEntitySpawned));
            Unsubscribe(nameof(OnItemRemovedFromContainer));
        }

        private void OnServerInitialized()
        {
            UpdateConfig();
            Subscribe(nameof(OnItemSplit));
            Subscribe(nameof(CanAcceptItem));
            Subscribe(nameof(OnItemRemovedFromContainer));
            if (configData.researchResource != "scrap")
            {
                researchResource = ItemManager.FindItemDefinition(configData.researchResource);
                if (researchResource != null)
                {
                    Subscribe(nameof(OnEntitySpawned));
                    foreach (var researchTable in BaseNetworkable.serverEntities.OfType<ResearchTable>())
                    {
                        if (defaultResearchResource != null)
                        {
                            defaultResearchResource = researchTable.researchResource;
                        }
                        OnEntitySpawned(researchTable);
                    }
                }
            }
            foreach (var player in BasePlayer.activePlayerList)
            {
                var researchTable = player.inventory?.loot?.entitySource as ResearchTable;
                if (researchTable != null)
                {
                    OnLootEntity(player, researchTable);
                }
            }
        }

        private void OnEntitySpawned(ResearchTable researchTable)
        {
            if (researchTable == null) return;
            researchTable.researchResource = researchResource;
            researchTable.SendNetworkUpdateImmediate();
        }

        private void Unload()
        {
            SaveData();
            foreach (var player in BasePlayer.activePlayerList)
            {
                DestroyAllUI(player);
            }
            if (configData.researchResource != "scrap")
            {
                researchResource = defaultResearchResource;
                foreach (var researchTable in BaseNetworkable.serverEntities.OfType<ResearchTable>())
                    OnEntitySpawned(researchTable);
            }
            instance = null;
        }

        private void OnServerSave() => timer.Once(UnityEngine.Random.Range(0f, 60f), SaveData);

        private object CanResearchItem(BasePlayer player, Item targetItem)
        {
            ConfigData.ResearchSettings researchS;
            if (configData.researchS.TryGetValue(targetItem.info.shortname, out researchS))
            {
                if (!researchS.canResearch)
                {
                    SendMessage(player, Lang("IsBlocked", player.UserIDString, GetItemDisplayName(player, targetItem.info, researchS.displayName)));
                    return false;
                }
                var scrapItem = GetScrapItem(targetItem.parent?.entityOwner as ResearchTable);
                if (scrapItem == null) return null;
                if (scrapItem.amount < researchS.scrapAmount)
                {
                    SendMessage(player, Lang("ResearchCantAfford", player.UserIDString, GetItemDisplayName(player, targetItem.info, researchS.displayName), researchS.scrapAmount));
                    return false;
                }
            }
            return null;
        }

        private void OnItemResearch(ResearchTable researchTable, Item targetItem, BasePlayer player)
        {
            researchers[researchTable] = player;
            ConfigData.ResearchSettings researchS;
            if (configData.researchS.TryGetValue(targetItem.info.shortname, out researchS))
            {
                if (!researchS.canResearch) return;
                researchTable.researchDuration = researchS.researchTime;
                if (researchS.researchTime > 0)
                {
                    float timeLeft;
                    Timer researchTimer = null;
                    researchTimer = timer.Repeat(0.1f, (int)(researchS.researchTime / 0.1f) + 10, () =>
                    {
                        if (researchTable == null || researchTable.IsDestroyed)
                        {
                            researchTimer?.Destroy();
                            return;
                        }
                        timeLeft = researchTable.researchFinishedTime - Time.realtimeSinceStartup;
                        if (timeLeft <= 0)
                        {
                            researchTimer?.Destroy();
                            UpdateUI(researchTable.inventory, null);
                            return;
                        }

                        HashSet<BasePlayer> looters;
                        if (lootingPlayers.TryGetValue(researchTable, out looters))
                        {
                            foreach (var p in looters)
                            {
                                CreateResearchUI(p, timeLeft.ToString("0.00"), configData.uiS.researchTimeleftTextSize);
                            }
                        }
                        else if (player != null && player.inventory?.loot?.entitySource == researchTable)
                        {
                            CreateResearchUI(player, timeLeft.ToString("0.00"), configData.uiS.researchTimeleftTextSize);
                        }
                    });
                }
            }
        }

        private object OnItemResearched(ResearchTable researchTable, int chance)
        {
            var targetItem = researchTable.GetTargetItem();
            ConfigData.ResearchSettings researchS;
            if (configData.researchS.TryGetValue(targetItem.info.shortname, out researchS))
            {
                if (!researchS.canResearch) return null;
                BasePlayer researcher;
                if (researchers.TryGetValue(researchTable, out researcher))
                {
                    researchers.Remove(researchTable);
                }
                else
                {
                    researcher = researchTable.user;
                }

                HashSet<BasePlayer> players;
                if (!lootingPlayers.TryGetValue(researchTable, out players))
                {
                    players = new HashSet<BasePlayer> { researcher };
                }

                if (UnityEngine.Random.Range(0f, 100f) < researchS.successChance)
                {
                    foreach (var player in players)
                    {
                        if (player == null) continue;
                        SendMessage(player, Lang("ResearchSuccess", player.UserIDString, GetItemDisplayName(player, targetItem.info, researchS.displayName)));
                    }
                    return researchS.scrapAmount;
                }
                else
                {
                    var scrapItem = researchTable.GetScrapItem();
                    TryConsumeItem(researcher, scrapItem, targetItem, researchS, players);
                    return scrapItem.amount + 1;
                }
            }
            return null;
        }

        private void OnLootEntity(BasePlayer player, ResearchTable researchTable)
        {
            if (player == null || researchTable == null) return;
            HashSet<BasePlayer> looters;
            if (!lootingPlayers.TryGetValue(researchTable, out looters))
            {
                looters = new HashSet<BasePlayer> { player };
                lootingPlayers.Add(researchTable, looters);
            }
            else
            {
                looters.Add(player);
            }
            UpdateUI(researchTable.inventory, null);
        }

        private void OnLootEntityEnd(BasePlayer player, ResearchTable researchTable)
        {
            if (player == null || researchTable == null) return;
            HashSet<BasePlayer> players;
            if (lootingPlayers.TryGetValue(researchTable, out players))
            {
                players.Remove(player);
                if (players.Count <= 0)
                {
                    lootingPlayers.Remove(researchTable);
                }
                DestroyAllUI(player);
            }
        }

        private void OnItemSplit(Item item, int splitAmount) => NextTick(() => UpdateUI(null, item));

        private void CanAcceptItem(ItemContainer itemContainer, Item item, int targetPos) => NextTick(() => UpdateUI(itemContainer, item));

        private void OnItemRemovedFromContainer(ItemContainer itemContainer, Item item) => NextTick(() => UpdateUI(itemContainer, item));

        #endregion Oxide Hooks

        #region Methods

        private void UpdateConfig()
        {
            var researchTable = GameManager.server.FindPrefab(PREFAB_RESEARCH_TABLE)?.GetComponent<ResearchTable>();
            if (researchTable == null) return;
            var newResearchS = new Dictionary<string, ConfigData.ResearchSettings>();
            foreach (var itemDefinition in ItemManager.GetItemDefinitions())
            {
                var item = ItemManager.CreateByItemID(itemDefinition.itemid);
                if (researchTable.IsItemResearchable(item))
                {
                    ConfigData.ResearchSettings researchS;
                    if (configData.researchS.TryGetValue(item.info.shortname, out researchS))
                    {
                        newResearchS.Add(itemDefinition.shortname, researchS);
                    }
                    else
                    {
                        newResearchS.Add(itemDefinition.shortname, new ConfigData.ResearchSettings
                        {
                            displayName = itemDefinition.displayName.english,
                            scrapAmount = ResearchTable.ScrapForResearch(item),
                            itemConsumedSettings = new Dictionary<int, ConfigData.ConsumeSettings>
                            {
                                [1] = new ConfigData.ConsumeSettings()
                            }
                        });
                    }
                }
                item.Remove();
            }
            ItemManager.DoRemoves();
            configData.researchS = newResearchS;
            SaveConfig();
        }

        private void UpdateUI(ItemContainer itemContainer, Item item)
        {
            var researchTable = itemContainer?.entityOwner as ResearchTable;
            if (researchTable == null)
            {
                researchTable = item?.parent?.entityOwner as ResearchTable;
            }

            if (researchTable == null) return;
            int scrapAmount = 0;
            bool flag = CanResearch(researchTable, ref scrapAmount);
            HashSet<BasePlayer> looters;
            if (lootingPlayers.TryGetValue(researchTable, out looters))
            {
                foreach (var player in looters)
                {
                    CreateCostUI(player, scrapAmount);
                    if (!researchTable.IsResearching())
                        CreateResearchUI(player, flag ? string.Empty : Lang("CantResearch", player.UserIDString));
                }
            }
        }

        private bool CanResearch(ResearchTable researchTable, ref int scrapAmount)
        {
            var targetItem = researchTable.GetTargetItem();
            if (targetItem != null && targetItem.amount <= 1 && !targetItem.isBroken && researchTable.IsItemResearchable(targetItem))
            {
                ConfigData.ResearchSettings researchS;
                if (configData.researchS.TryGetValue(targetItem.info.shortname, out researchS))
                {
                    if (researchS.canResearch)
                    {
                        scrapAmount = researchS.scrapAmount;
                        var scrapItem = GetScrapItem(researchTable);
                        if (scrapItem != null && scrapItem.amount >= researchS.scrapAmount)
                        {
                            return true;
                        }
                    }
                }
            }
            return false;
        }

        private void TryConsumeItem(BasePlayer researcher, Item scrapItem, Item targetItem, ConfigData.ResearchSettings researchS, HashSet<BasePlayer> players)
        {
            var failures = GetNumberOfFailures(researcher.userID, targetItem.info.shortname);
            var consumeS = GetConsumeSettings(researchS, failures);
            if (UnityEngine.Random.Range(0f, 100f) < consumeS.scrapChance)
            {
                if (consumeS.scrapPercentage > 0)
                {
                    var amount = (int)Math.Round(researchS.scrapAmount * consumeS.scrapPercentage / 100);
                    TakeItem(scrapItem, amount);
                    foreach (var player in players)
                    {
                        if (player == null) continue;
                        CreateResearchUI(player);
                        SendMessage(player, Lang("ResearchFailedScrap", player.UserIDString, GetItemDisplayName(player, targetItem.info, researchS.displayName), amount));
                    }
                }
                else
                {
                    foreach (var player in players)
                    {
                        if (player == null) continue;
                        CreateResearchUI(player);
                        SendMessage(player, Lang("ResearchFailed", player.UserIDString, GetItemDisplayName(player, targetItem.info, researchS.displayName)));
                    }
                }
            }

            if (UnityEngine.Random.Range(0f, 100f) < consumeS.targetItemChance)
            {
                TakeItem(targetItem, targetItem.amount);
                foreach (var player in players)
                {
                    if (player == null) continue;
                    CreateResearchUI(player);
                    SendMessage(player, Lang("ResearchFailedTargetItem", player.UserIDString, GetItemDisplayName(player, targetItem.info, researchS.displayName)));
                }
            }
        }

        private int GetNumberOfFailures(ulong playerID, string shortname)
        {
            Dictionary<string, int> data;
            if (!storedData.playerResearchFailures.TryGetValue(playerID, out data))
            {
                data = new Dictionary<string, int>();
                storedData.playerResearchFailures.Add(playerID, data);
            }

            int failures;
            if (!data.TryGetValue(shortname, out failures))
            {
                failures = 1;
                data.Add(shortname, failures);
            }
            else
            {
                failures++;
                data[shortname] = failures;
            }
            return failures;
        }

        private static void DoResearch(ResearchTable researchTable, BasePlayer player)
        {
            BaseEntity.RPCMessage rpcMessage = default(BaseEntity.RPCMessage);
            rpcMessage.player = player;
            researchTable.DoResearch(rpcMessage);
        }

        private static Item GetScrapItem(ResearchTable researchTable)
        {
            var slot = researchTable?.inventory?.GetSlot(1);
            if (slot == null || slot.info != researchTable.researchResource)
                return null;
            return slot;
        }

        private static void TakeItem(Item item, int amount)
        {
            if (item.amount <= amount)
            {
                item.RemoveFromContainer();
                item.Remove();
            }
            else item.UseItem(amount);
        }

        private static ConfigData.ConsumeSettings GetConsumeSettings(ConfigData.ResearchSettings researchS, int failures)
        {
            return researchS.itemConsumedSettings.OrderByDescending(x => x.Key).FirstOrDefault(x => x.Key <= failures).Value;
        }

        private void SendMessage(BasePlayer player, string message)
        {
            if (configData.chatS.usePop && PopupNotifications != null) CreatePopupNotification(message, player);
            else Print(player, message);
        }

        private void CreatePopupNotification(string message, BasePlayer player = null, float duration = 0f) => PopupNotifications?.Call("CreatePopupNotification", message, player, duration);

        #region RustTranslationAPI

        private string GetItemTranslationByShortName(string language, string itemShortName) => (string)RustTranslationAPI.Call("GetItemTranslationByShortName", language, itemShortName);

        private string GetItemDisplayName(BasePlayer player, ItemDefinition itemDefinition, string displayName)
        {
            if (RustTranslationAPI != null)
            {
                var displayName1 = GetItemTranslationByShortName(lang.GetLanguage(player.UserIDString), itemDefinition.shortname);
                if (!string.IsNullOrEmpty(displayName1))
                {
                    return displayName1;
                }
            }

            return displayName;
        }

        #endregion RustTranslationAPI

        #endregion Methods

        #region UI

        private const string UINAME_COST = "ResearchUI_Cost";
        private const string UINAME_RESEARCH = "ResearchUI_Research";

        public class UI
        {
            public static CuiElementContainer CreateElementContainer(string parent, string panelName, string backgroundColor, string anchorMin, string anchorMax, string offsetMin, string offsetMax, bool cursor = false)
            {
                return new CuiElementContainer()
                {
                    {
                        new CuiPanel
                        {
                            Image = { Color = backgroundColor },
                            RectTransform = { AnchorMin = anchorMin, AnchorMax = anchorMax ,OffsetMin = offsetMin,OffsetMax = offsetMax},
                            CursorEnabled = cursor
                        },  parent,  panelName
                    }
                };
            }

            public static void CreateLabel(ref CuiElementContainer container, string panelName, string textColor, string text, int fontSize, string anchorMin, string anchorMax, TextAnchor align = TextAnchor.MiddleCenter, float fadeIn = 0f)
            {
                container.Add(new CuiLabel
                {
                    Text = { Color = textColor, FontSize = fontSize, Align = align, Text = text, FadeIn = fadeIn },
                    RectTransform = { AnchorMin = anchorMin, AnchorMax = anchorMax }
                }, panelName, CuiHelper.GetGuid());
            }

            public static void CreateButton(ref CuiElementContainer container, string panelName, string buttonColor, string command, string textColor, string text, int fontSize, string anchorMin, string anchorMax, TextAnchor align = TextAnchor.MiddleCenter, float fadeIn = 0f)
            {
                container.Add(new CuiButton
                {
                    Button = { Color = buttonColor, Command = command },
                    RectTransform = { AnchorMin = anchorMin, AnchorMax = anchorMax },
                    Text = { Color = textColor, Text = text, FontSize = fontSize, Align = align, FadeIn = fadeIn }
                }, panelName, CuiHelper.GetGuid());
            }
        }

        private static void CreateResearchUI(BasePlayer player, string message = "", int fontSize = 0)
        {
            if (player == null) return;
            var container = UI.CreateElementContainer("Overlay", UINAME_RESEARCH, instance.configData.uiS.researchBackgroundColor, "0.5 0", "0.5 0", "436 116", "565 148");
            if (string.IsNullOrEmpty(message)) UI.CreateButton(ref container, UINAME_RESEARCH, "0 0 0 0", "ResearchUI_DoResearch", instance.configData.uiS.researchTextColor, instance.Lang("CanResearch", player.UserIDString), instance.configData.uiS.researchTextSize, "0 0", "1 1");
            else UI.CreateLabel(ref container, UINAME_RESEARCH, instance.configData.uiS.researchTextColor, message, fontSize == 0 ? instance.configData.uiS.researchTextSize : fontSize, "0 0", "1 1");
            CuiHelper.DestroyUi(player, UINAME_RESEARCH);
            CuiHelper.AddUi(player, container);
        }

        private static void CreateCostUI(BasePlayer player, int scrapAmount)
        {
            if (player == null) return;
            string message = scrapAmount > 0 ? scrapAmount.ToString() : "N/A";
            var container = UI.CreateElementContainer("Overlay", UINAME_COST, instance.configData.uiS.researchCostBackgroundColor, "0.5 0", "0.5 0", "445 292", "572 372");
            UI.CreateLabel(ref container, UINAME_COST, instance.configData.uiS.researchCostTextColor, message, instance.configData.uiS.researchCostTextSize, "0 0", "1 1");
            CuiHelper.DestroyUi(player, UINAME_COST);
            CuiHelper.AddUi(player, container);
        }

        private static void DestroyAllUI(BasePlayer player)
        {
            CuiHelper.DestroyUi(player, UINAME_COST);
            CuiHelper.DestroyUi(player, UINAME_RESEARCH);
        }

        #endregion UI

        #region Commands

        [ConsoleCommand("ResearchUI_DoResearch")]
        private void CCmdDoResearch(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player == null) return;
            var researchTable = player.inventory?.loot?.entitySource as ResearchTable;
            int scrapAmount = 0;
            if (researchTable != null && CanResearch(researchTable, ref scrapAmount))
            {
                DoResearch(researchTable, player);
            }
        }

        [ConsoleCommand("br.lvl")]
        private void CCmdResearchS(ConsoleSystem.Arg arg)
        {
            if (arg != null && arg.HasArgs(3) && arg.IsAdmin)
            {
                int level;
                if (int.TryParse(arg.Args[0], out level))
                {
                    var blueprintLevels = ItemManager.GetBlueprints().GroupBy(x => x.workbenchLevelRequired).ToDictionary(x => x.Key, y => y.Select(x => x.targetItem.shortname));
                    IEnumerable<string> itemBlueprints;
                    if (blueprintLevels.TryGetValue(level, out itemBlueprints))
                    {
                        switch (arg.Args[1].ToLower())
                        {
                            case "cost":
                                int cost;
                                if (!int.TryParse(arg.Args[2], out cost) || cost <= 0) break;
                                foreach (var entry in configData.researchS)
                                {
                                    if (itemBlueprints.Contains(entry.Key))
                                        entry.Value.scrapAmount = cost;
                                }
                                Print(arg, $"Successfully modified the research cost of level {level} workbench items.");
                                SaveConfig();
                                return;

                            case "time":
                                float time;
                                if (!float.TryParse(arg.Args[2], out time) || time < 0) break;
                                foreach (var entry in configData.researchS)
                                {
                                    if (itemBlueprints.Contains(entry.Key))
                                        entry.Value.researchTime = time;
                                }
                                Print(arg, $"Successfully modified the research time of level {level} workbench items.");
                                SaveConfig();
                                return;

                            case "successchance":
                                float chance;
                                if (!float.TryParse(arg.Args[2], out chance) || chance < 0) break;
                                foreach (var entry in configData.researchS)
                                {
                                    if (itemBlueprints.Contains(entry.Key))
                                        entry.Value.successChance = chance;
                                }
                                Print(arg, $"Successfully modified the chance of research success of level {level} workbench items.");
                                SaveConfig();
                                return;

                            case "scrapchance":
                                var scrapChances = new Dictionary<int, float>();
                                for (int i = 2; i < arg.Args.Length; i++)
                                {
                                    float value;
                                    if (float.TryParse(arg.Args[i], out value) && value >= 0)
                                    {
                                        scrapChances.Add(i - 1, value);
                                    }
                                }
                                foreach (var entry in configData.researchS)
                                {
                                    if (itemBlueprints.Contains(entry.Key))
                                    {
                                        foreach (var entry1 in scrapChances)
                                        {
                                            ConfigData.ConsumeSettings consumeS;
                                            if (entry.Value.itemConsumedSettings.TryGetValue(entry1.Key, out consumeS))
                                            {
                                                consumeS.scrapChance = entry1.Value;
                                            }
                                            else
                                            {
                                                consumeS = new ConfigData.ConsumeSettings { scrapChance = entry1.Value };
                                                entry.Value.itemConsumedSettings.Add(entry1.Key, consumeS);
                                            }
                                        }
                                    }
                                }
                                Print(arg, $"Successfully modified the chance of scrap consumed when research fails in level {level} workbench items.");
                                SaveConfig();
                                return;

                            case "scrappercentage":
                                var scrapPercentages = new Dictionary<int, float>();
                                for (int i = 2; i < arg.Args.Length; i++)
                                {
                                    float value;
                                    if (float.TryParse(arg.Args[i], out value) && value >= 0)
                                    {
                                        scrapPercentages.Add(i - 1, value);
                                    }
                                }
                                foreach (var entry in configData.researchS)
                                {
                                    if (itemBlueprints.Contains(entry.Key))
                                    {
                                        foreach (var entry1 in scrapPercentages)
                                        {
                                            ConfigData.ConsumeSettings consumeS;
                                            if (entry.Value.itemConsumedSettings.TryGetValue(entry1.Key, out consumeS))
                                            {
                                                consumeS.scrapPercentage = entry1.Value;
                                            }
                                            else
                                            {
                                                consumeS = new ConfigData.ConsumeSettings { scrapPercentage = entry1.Value };
                                                entry.Value.itemConsumedSettings.Add(entry1.Key, consumeS);
                                            }
                                        }
                                    }
                                }
                                Print(arg, $"Successfully modified the percentage of scrap amount consumed when research fails in level {level} workbench items.");
                                SaveConfig();
                                return;

                            case "targetitemchance":
                                var targetItemChances = new Dictionary<int, float>();
                                for (int i = 2; i < arg.Args.Length; i++)
                                {
                                    float value;
                                    if (float.TryParse(arg.Args[i], out value) && value >= 0)
                                    {
                                        targetItemChances.Add(i - 1, value);
                                    }
                                }
                                foreach (var entry in configData.researchS)
                                {
                                    if (itemBlueprints.Contains(entry.Key))
                                    {
                                        foreach (var entry1 in targetItemChances)
                                        {
                                            ConfigData.ConsumeSettings consumeS;
                                            if (entry.Value.itemConsumedSettings.TryGetValue(entry1.Key, out consumeS))
                                            {
                                                consumeS.targetItemChance = entry1.Value;
                                            }
                                            else
                                            {
                                                consumeS = new ConfigData.ConsumeSettings { targetItemChance = entry1.Value };
                                                entry.Value.itemConsumedSettings.Add(entry1.Key, consumeS);
                                            }
                                        }
                                    }
                                }
                                Print(arg, $"Successfully modified the chance of target item consumed when research fails in level {level} workbench items.");
                                SaveConfig();
                                return;
                        }
                    }
                }
            }
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.AppendLine("Syntax error");
            stringBuilder.AppendLine("br.lvl <WorkBenchLevel (0~3)> <cost> <(cost amount)> - Modify the research cost of level N workbench items. e.g.: 'br.lvl 3 cost 750'");
            stringBuilder.AppendLine("br.lvl <WorkBenchLevel (0~3)> <time> <(research time)> - Modify the research time of level N workbench items. e.g.: 'br.lvl 3 time 2'");
            stringBuilder.AppendLine("br.lvl <WorkBenchLevel (0~3)> <successChance> <(success chance)> - Modify the chance of research success of level N workbench items. e.g.: 'br.lvl 3 successChance 50'");
            stringBuilder.AppendLine("br.lvl <WorkBenchLevel (0~3)> <scrapChance> [Chance of first failure] [Chance of second failure]... - Modify the chance of scrap consumed when research fails of level N workbench items. e.g.: 'br.lvl 3 scrapChance 50' or 'br.lvl 3 scrapChance 50 20'");
            stringBuilder.AppendLine("br.lvl <WorkBenchLevel (0~3)> <scrapPercentage> [Percentage of first failure] [Percentage of second failure]... - Modify the percentage of scrap amount consumed when research fails of level N workbench items. e.g.: 'br.lvl 3 scrapPercentage 50' or 'br.lvl 3 scrapPercentage 50 20'");
            stringBuilder.AppendLine("br.lvl <WorkBenchLevel (0~3)> <targetItemChance> [Chance of first failure] [Chance of second failure]... - Modify the chance of target item consumed when research fails of level N workbench items. e.g.: 'br.lvl 3 targetItemChance 50' or 'br.lvl 3 targetItemChance 50 20'");
            Print(arg, stringBuilder.ToString());
        }

        #endregion Commands

        #region ConfigurationFile

        private ConfigData configData;

        private class ConfigData
        {
            [JsonProperty(PropertyName = "Research Resource (Item Short Name)")]
            public string researchResource = "scrap";

            [JsonProperty(PropertyName = "Chat Settings")]
            public ChatSettings chatS = new ChatSettings();

            [JsonProperty(PropertyName = "Research UI Settings")]
            public UISettings uiS = new UISettings();

            [JsonProperty(PropertyName = "Research Settings")]
            public Dictionary<string, ResearchSettings> researchS = new Dictionary<string, ResearchSettings>();

            [JsonProperty(PropertyName = "Version")]
            public VersionNumber version;

            public class ChatSettings
            {
                [JsonProperty(PropertyName = "Use PopupNotifications")]
                public bool usePop;

                [JsonProperty(PropertyName = "Chat Prefix")]
                public string prefix = "<color=#00FFFF>[BetterResearching]</color>: ";

                [JsonProperty(PropertyName = "Chat SteamID Icon")]
                public ulong steamIDIcon;
            }

            public class UISettings
            {
                [JsonProperty(PropertyName = "Research - Box - Background Color")]
                public string researchBackgroundColor = "0.42 0.52 0.25 0.98";

                [JsonProperty(PropertyName = "Research - Text - Text Color")]
                public string researchTextColor = "0 0 0 1";

                [JsonProperty(PropertyName = "Research - Text - Text Size")]
                public int researchTextSize = 16;

                [JsonProperty(PropertyName = "Research - Text - Timeleft Text Size")]
                public int researchTimeleftTextSize = 26;

                [JsonProperty(PropertyName = "ResearchCost - Box - Background Color")]
                public string researchCostBackgroundColor = "0 0 0 0.98";

                [JsonProperty(PropertyName = "ResearchCost - Text - Text Color")]
                public string researchCostTextColor = "1 0 0 1";

                [JsonProperty(PropertyName = "ResearchCost - Text - Text Size")]
                public int researchCostTextSize = 50;
            }

            public class ResearchSettings
            {
                [JsonProperty(PropertyName = "Can Research")]
                public bool canResearch = true;

                [JsonProperty(PropertyName = "Display Name")]
                public string displayName = string.Empty;

                [JsonProperty(PropertyName = "Research Cost")]
                public int scrapAmount;

                [JsonProperty(PropertyName = "Research Time")]
                public float researchTime = 10f;

                [JsonProperty(PropertyName = "Research Success Chance")]
                public float successChance = 100f;

                [JsonProperty(PropertyName = "Item Consumed When Research Fails")]
                public Dictionary<int, ConsumeSettings> itemConsumedSettings = new Dictionary<int, ConsumeSettings>();
            }

            public class ConsumeSettings
            {
                [JsonProperty(PropertyName = "Scrap Consumed Chance")]
                public float scrapChance = 100f;

                [JsonProperty(PropertyName = "Percentage Of Scrap Amount Consumed")]
                public float scrapPercentage = 100f;

                [JsonProperty(PropertyName = "Target Item Consumed Chance")]
                public float targetItemChance = 0f;
            }
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                configData = Config.ReadObject<ConfigData>();
                if (configData == null)
                {
                    LoadDefaultConfig();
                }
                else
                {
                    UpdateConfigValues();
                }
            }
            catch (Exception ex)
            {
                PrintError($"The configuration file is corrupted. \n{ex}");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void LoadDefaultConfig()
        {
            PrintWarning("Creating a new configuration file");
            configData = new ConfigData();
            configData.version = Version;
        }

        protected override void SaveConfig() => Config.WriteObject(configData);

        private void UpdateConfigValues()
        {
            if (configData.version < Version)
            {
                if (configData.version <= default(VersionNumber))
                {
                    string prefix, prefixColor;
                    if (GetConfigValue(out prefix, "Chat Settings", "Chat Prefix") && GetConfigValue(out prefixColor, "Chat Settings", "Chat Prefix Color"))
                    {
                        configData.chatS.prefix = $"<color={prefixColor}>{prefix}</color>: ";
                    }
                }
                configData.version = Version;
            }
        }

        private bool GetConfigValue<T>(out T value, params string[] path)
        {
            var configValue = Config.Get(path);
            if (configValue == null)
            {
                value = default(T);
                return false;
            }
            value = Config.ConvertValue<T>(configValue);
            return true;
        }

        #endregion ConfigurationFile

        #region DataFile

        private StoredData storedData;

        private class StoredData
        {
            public readonly Dictionary<ulong, Dictionary<string, int>> playerResearchFailures = new Dictionary<ulong, Dictionary<string, int>>();
        }

        private void LoadData()
        {
            try
            {
                storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
            }
            catch
            {
                storedData = null;
            }

            if (storedData == null)
            {
                ClearData();
            }
        }

        private void ClearData()
        {
            storedData = new StoredData();
            SaveData();
        }

        private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(Name, storedData);

        private void OnNewSave(string filename) => ClearData();

        #endregion DataFile

        #region LanguageFile

        private void Print(BasePlayer player, string message) => Player.Message(player, message, configData.chatS.prefix, configData.chatS.steamIDIcon);

        private void Print(ConsoleSystem.Arg arg, string message)
        {
            var player = arg?.Player();
            if (player == null) Puts(message);
            else PrintToConsole(player, message);
        }

        private string Lang(string key, string id = null, params object[] args)
        {
            try
            {
                return string.Format(lang.GetMessage(key, this, id), args);
            }
            catch (Exception)
            {
                PrintError($"Error in the language formatting of '{key}'. (userid: {id}. lang: {lang.GetLanguage(id)}. args: {string.Join(" ,", args)})");
                throw;
            }
        }

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["ResearchFailed"] = "Researching <color=#FF8C00>{0}</color> failed",
                ["ResearchFailedScrap"] = "Researching <color=#FF8C00>{0}</color> failed，Consumed {1} Scrap",
                ["ResearchFailedTargetItem"] = "Researching <color=#FF8C00>{0}</color> failed，target item is consumed",
                ["ResearchSuccess"] = "Researched <color=#7FFF00>{0}</color> successfully ",
                ["ResearchCantAfford"] = "You don't have enough Scrap to research {0}. This item's research cost is {1} Scrap",
                ["IsBlocked"] = "<color=#1E90FF>{0}</color> is blocked, cannot research",
                ["CantResearch"] = "CANT RESEARCH",
                ["CanResearch"] = "BEGIN RESEARCH",
            }, this);
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["ResearchFailed"] = "<color=#FF8C00>{0}</color> 研究失败",
                ["ResearchFailedAmount"] = "<color=#FF8C00>{0}</color> 研究失败，浪费了 {1} 个废料",
                ["ResearchFailedTargetItem"] = "<color=#FF8C00>{0}</color> 研究失败，目标物品已经破碎了",
                ["ResearchSuccess"] = "<color=#7FFF00>{0}</color> 研究成功",
                ["ResearchCantAfford"] = "您需要 {1} 个废料来研究 <color=#7FFF00>{0}</color>",
                ["IsBlocked"] = "服务器禁止您研究 <color=#1E90FF>{0}</color>",
                ["CantResearch"] = "无法研究",
                ["CanResearch"] = "开始研究",
            }, this, "zh-CN");
        }

        #endregion LanguageFile
    }
}