﻿using System.Collections.Generic;
using System.Collections;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System;
using Oxide.Game.Rust.Cui;
using Oxide.Core.Plugins;
using Oxide.Core;
using Newtonsoft.Json;
using UnityEngine.UI;
using UnityEngine;

namespace Oxide.Plugins;

[Info("Tree Planter", "Bazz3l", "1.2.10")]
[Description("Give players the ability to plant trees, make your very own wood farm or just enjoy the nature")]
internal class TreePlanter : RustPlugin
{
    [PluginReference] private Plugin ServerRewards, ImageLibrary, Economics, Clans;

    #region Fields

    private const string PERM_USE = "treeplanter.use";
    private const string PERM_MENU = "treeplanter.menu";
    private const int ICON_ITEM_ID = -886280491;
    private const int PLANT_LAYER_MASK = (int)(
        Rust.Layers.Mask.Construction | Rust.Layers.Mask.Player_Server    |
        Rust.Layers.Mask.Tree         | Rust.Layers.Mask.Bush             |
        Rust.Layers.Mask.Harvestable  | Rust.Layers.Mask.Vehicle_Detailed
    );
    
    private readonly Dictionary<ulong, Timer> _menuPopup = new();
    private readonly Dictionary<ulong, int> _menuPages = new();
    private CuiElementContainer _menuLayout;
    private DeployVolume[] _deployVolumes;
    private UiGrid _menuGrid = new();
    private ConfigData _configData;
    private int _totalPages;

    #endregion
    
    #region Lang

    protected override void LoadDefaultMessages()
    {
        lang.RegisterMessages(new Dictionary<string, string>
        {
            {LangKeys.MESSAGE_PREFIX, "<color=#DC143C>Tree Planter</color>: "},
            {LangKeys.ERROR_NO_PERMISSION, "Sorry, you don't have permission to perform that action."},
            {LangKeys.ERROR_MISSING_PRIVILEGE, "Sorry, you need build privilege to perform this action."},
            {LangKeys.ERROR_MISSING_BALANCE, "You don't have enough for this transaction."},
            {LangKeys.ERROR_PLANTER_REQUIRED, "This item must be planted in the planter."},
            {LangKeys.ERROR_GROUND_REQUIRED, "This item must be planted in the ground."},
            {LangKeys.ERROR_MISSING_ITEM, "Sorry, we couldn't find an item by that name."},
            {LangKeys.ERROR_INSIDE_OBJECT, "Sorry, you can't deploy inside objects."},
            
            {LangKeys.MESSAGE_INFO, "Please choose one of the available options below."},
            {LangKeys.MESSAGE_ITEM, "{0} - ${1}"},
            {LangKeys.MESSAGE_PLANTED, "<color=#FFC55C>{0}</color> was successfully planted."},
            {LangKeys.MESSAGE_RECEIVED, "You've purchased <color=#FFC55C>{0}x</color> <color=#FFC55C>{1}</color>."},
            
            {LangKeys.UI_HEADING_TEXT, "TREE PLANTER"},
            {LangKeys.UI_CLOSE_TEXT, "CLOSE"},
            {LangKeys.UI_ITEMS_TEXT, "{0}<br>{1} Cost<br>{2}x"},
            {LangKeys.UI_EMPTY_TEXT, "NOTHING TO SHOW HERE"},
            {LangKeys.UI_PREV_ICON, "◀"},
            {LangKeys.UI_NEXT_ICON, "▶"},
        }, this);
    }

    private static class LangKeys
    {
        public const string MESSAGE_PREFIX = "MESSAGE_PREFIX";
        public const string ERROR_NO_PERMISSION = "ERROR_NO_PERMISSION";
        public const string ERROR_MISSING_ITEM = "ERROR_MISSING_ITEM";
        public const string ERROR_MISSING_PRIVILEGE = "ERROR_MISSING_PRIVILEGE";
        public const string ERROR_INSIDE_OBJECT = "ERROR_INSIDE_OBJECT";
        public const string ERROR_MISSING_BALANCE = "ERROR_MISSING_BALANCE";
        public const string ERROR_PLANTER_REQUIRED = "ERROR_PLANTER_REQUIRED";
        public const string ERROR_GROUND_REQUIRED = "ERROR_GROUND_REQUIRED";
        
        public const string MESSAGE_ITEM = "MESSAGE_ITEM";
        public const string MESSAGE_INFO = "MESSAGE_INFO";
        public const string MESSAGE_PLANTED = "MESSAGE_PLANTED";
        public const string MESSAGE_RECEIVED = "MESSAGE_RECEIVED";
        
        public const string UI_HEADING_TEXT = "UI_HEADING_TEXT";
        public const string UI_EMPTY_TEXT = "UI_EMPTY_TEXT";
        public const string UI_CLOSE_TEXT = "UI_CLOSE_TEXT";
        public const string UI_ITEMS_TEXT = "UI_ITEMS_TEXT";
        public const string UI_PREV_ICON = "UI_PREV_ICON";
        public const string UI_NEXT_ICON = "UI_NEXT_ICON";
    }
        
    private string GetMessage(string key, string id = null, params object[] args)
    {
        return args?.Length > 0 
            ? string.Format(lang.GetMessage(key, this, id), args) 
            : lang.GetMessage(key, this, id);
    }

    private static void MessagePlayer(BasePlayer player, string message)
    {
        if (player?.net == null) 
            return;
        
        player.ChatMessage(message);
    }
        
    #endregion

    #region Config

    protected override void LoadDefaultConfig() => _configData = ConfigData.DefaultConfig(Version);

    protected override void LoadConfig()
    {
        base.LoadConfig();

        try
        {
            _configData = Config.ReadObject<ConfigData>() ?? throw new JsonException();
            
            bool wasUpdated = false;

            if (_configData.Version == null)
            {
                _configData.Version = Version;
                wasUpdated = true;
            }
            
            if (wasUpdated)
            {
                SaveConfig();
                PrintWarning("Config has been updated.");
            }
        }
        catch(Exception e)
        {
            LoadDefaultConfig();
            PrintWarning("Loaded default config: {0}", e.Message);
        }

        UpdateConfig();
    }

    protected override void SaveConfig() => Config.WriteObject(_configData, true);

    private void UpdateConfig()
    {
        HashSet<string> forbiddenPrefabs = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "dead", "trumpet", "vine"
        };
        
        if (_configData.AgriBlocked.Count == 0)
        {
            ItemManager.Initialize();
            
            foreach (ItemDefinition iDef in ItemManager.itemList)
            {
                if (iDef.shortname != null && iDef.shortname.Contains("seed")) 
                    _configData.AgriBlocked[iDef.shortname] = true;
            }
        }
        
        if (_configData.TreeConfigs.Count == 0)
        {
            foreach (KeyValuePair<string, GameObject> valuePair in Prefab.DefaultManager.preProcessed.prefabList)
            {
                if (valuePair.Value == null || !valuePair.Value.TryGetComponent(out TreeEntity entity))
                    continue;
                
                if (string.IsNullOrEmpty(entity.ShortPrefabName) || string.IsNullOrEmpty(entity.PrefabName))
                    continue;
                
                if (forbiddenPrefabs.Any(entity.PrefabName.Contains))
                    continue;
                
                string prefabName = entity.ShortPrefabName.Replace("_", "-");
                if (string.IsNullOrEmpty(prefabName))
                    continue;
                
                if (_configData.TreeConfigs.Exists(treeConfig => treeConfig.Name == prefabName)) 
                    continue;
                
                _configData.TreeConfigs.Add(TreeConfig.CreateNew(prefabName, entity.PrefabName));
            }
        }
        
        _configData.TreeConfigs.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase));
        
        SaveConfig();
    }

    private class ConfigData
    {
        [JsonProperty("UseCurrency (use custom items as currency, by specifying the CurrencyItem)")]
        public bool UseCurrencyItem;
        [JsonProperty("UseServerRewards (use server rewards as currency)")]
        public bool UseServerRewards;
        [JsonProperty("UseEconomics (use economics as currency)")]
        public bool UseEconomics;
        
        [JsonProperty("CurrencyItem (set an item id to use as currency, default is set to scrap)")]
        public int CurrencyItemID;
        [JsonProperty("CurrencySkinID (set an skin id to use as currency, default is set to 0)")]
        public ulong CurrencySkinID;
        
        [JsonProperty("EnableOwner (enables owners to chop down trees)")]
        public bool EnableOwner;
        [JsonProperty("EnableClan (enables clan members to chop down trees)")]
        public bool EnableClan;
        [JsonProperty("EnableTeam (enables team members to chop down trees)")]
        public bool EnableTeam;
        
        [JsonProperty("EnableGrowing (enables growing from saplings to adult trees over a period of time)")]
        public bool EnableGrowing;
        
        [JsonProperty("BlockedAgriItems (specify which items should only be placed in a planter box)")]
        public Dictionary<string, bool> AgriBlocked;
        
        [JsonProperty("TreeConfigs (list of available trees to purchase)")]
        public List<TreeConfig> TreeConfigs;
        
        [JsonProperty("current version number - (WARNING!!! do not change this value)")]
        public VersionNumber? Version;
        
        public TreeConfig? FindTreeConfig(string name)
        {
            if (string.IsNullOrEmpty(name)) 
                return null;

            foreach (TreeConfig treeConfig in TreeConfigs)
            {
                if (treeConfig.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || treeConfig.DisplayName.Equals(name, StringComparison.OrdinalIgnoreCase))
                    return treeConfig;
            }
                
            return null;
        }
            
        public static ConfigData DefaultConfig(VersionNumber version)
        {
            return new ConfigData
            {
                UseCurrencyItem = true,
                
                CurrencyItemID = -932201673,
                CurrencySkinID = 0,
                
                UseEconomics = false,
                UseServerRewards = false,
                
                EnableOwner = false,
                EnableClan = false,
                EnableTeam = false,
                
                EnableGrowing = false,
                
                AgriBlocked = new Dictionary<string, bool>
                {
                    {"seed.black.berry", true},
                    {"seed.blue.berry", true},
                    {"seed.green.berry", true},
                    {"seed.yellow.berry", true},
                    {"seed.white.berry", true},
                    {"seed.red.berry", true},
                    {"seed.corn", true},
                    {"clone.corn", true},
                    {"seed.pumpkin", true},
                    {"clone.pumpkin", true},
                    {"seed.hemp", true},
                    {"clone.hemp", true}
                },
                
                TreeConfigs = new List<TreeConfig>(),
                Version = version
            };
        } 
    }

    private class TreeConfig
    {
        public string Prefab;
        public string Name;
        public ulong Skin;
        public int Cost;
        public int Amount;
        [JsonProperty("ImageUrl", NullValueHandling = NullValueHandling.Ignore)]
        public string ImageUrl;

        [JsonIgnore]
        private string _displayName;

        [JsonIgnore]
        public string DisplayName
        {
            get
            {
                if (string.IsNullOrEmpty(_displayName))
                    _displayName = Name.Replace("-", " ").TitleCase();
                
                return _displayName;
            }
        }

        public void GiveItem(BasePlayer player, int amount = 1, BaseEntity.GiveItemReason reason = BaseEntity.GiveItemReason.Generic, GiveItemOptions options = GiveItemOptions.None)
        {
            Item item = ItemManager.CreateByItemID(ICON_ITEM_ID, amount, Skin);
            item.text = item.name = Name;
            //item.info.stackable = 1;
            item.MarkDirty();
            
            if (player.inventory.GiveItem(item, null, options))
            {
                bool infoBool = player.GetInfoBool("global.streamermode", false);
                string name = item.GetName(new bool?(infoBool));
                player.Command("note.inv", item.info.itemid, amount, string.IsNullOrEmpty(name) ? name : string.Empty, (int) reason);
                return;
            }
            
            item.Drop(player.inventory.containerMain.dropPosition, player.inventory.containerMain.dropVelocity);
        }

        public static TreeConfig CreateNew(string name, string prefab)
        {
            TreeConfig treeConfig = new TreeConfig();
            treeConfig.Name = name;
            treeConfig.Prefab = prefab;
            treeConfig.Cost = 10;
            treeConfig.Skin = 0UL;
            treeConfig.Amount = 1;
            return treeConfig;
        }
    }

    #endregion

    #region Oxide Hooks

    private void OnServerInitialized()
    {
        CachedMenuUI();
        GrowableManager.Initialize();
        DeployableVolume();
        NextFrame(CacheImages);
        
        if (_configData.EnableClan || _configData.EnableTeam || _configData.EnableOwner)
            Subscribe(nameof(OnMeleeAttack));
    }
    
    private void Init()
    {
        permission.RegisterPermission(PERM_USE, this);
        permission.RegisterPermission(PERM_MENU, this);
        
        Unsubscribe(nameof(OnMeleeAttack));
    }

    private void Unload()
    {
        DestroyMenuUI();
        GrowableManager.OnUnload();
    }

    private void OnPluginLoaded(Plugin plugin)
    {
        if (plugin?.Name == "ImageLibrary")
            NextFrame(CacheImages);
    }

    private void OnEntityBuilt(Planner planner, GameObject go)
    {
        if (planner == null || go == null || !planner.isTypeDeployable)
            return;
        
        BasePlayer player = planner.GetOwnerPlayer();
        if (player == null || !HasPermission(player, PERM_USE))
            return;
        
        if (!go.TryGetComponent<GrowableEntity>(out GrowableEntity entity))
            return;
        
        Item item = GetOwnerItem(planner);
        
        // Clone item details now, this is because the item is not available next frame ree reeee reeeeeeeeeeeee.
        PlantableClone plantableClone = new PlantableClone(item);
        
        // Wait needed here, why? because we need to wait for the planter box.
        NextFrame(() => HandlePlant(player, entity, plantableClone));
    }

    private object? OnMeleeAttack(BasePlayer player, HitInfo info)
    {
        if (player == null || info?.HitEntity is not TreeEntity entity || entity.OwnerID == 0UL)
            return null;
        
        if (!IsEntityOwner(entity.OwnerID, player.userID)) 
            return true;
            
        return null;
    }
    
    #endregion

    #region Core
    
    private class GrowableManager
    {
        public static GrowableManager Instance { get; private set; }
        public static readonly Vector3 GrowScale = new(0.01f, 0.01f, 0.01f);
        public static readonly Vector3 FullScale = new(1.0f, 1.0f, 1.0f);
        
        private readonly List<BaseEntity> _entities = new();
        private readonly Stopwatch _stopwatch = new();
        private Coroutine _routine;
        private bool _running;
        private const float TICK_INTERVAL = 10f;

        public static void Initialize()
        {
            Instance = new GrowableManager();
            Instance.GatherEntities();
        }

        public static void OnUnload()
        {
            if (Rust.Application.isQuitting)
                return;
            
            Instance?.Cancel();
            Instance = null;
        }

        public void Enqueue(BaseEntity entity)
        {
            _entities.Add(entity);
                
            if (_running)
                return;

            _running = true;
            _routine = ServerMgr.Instance.StartCoroutine(CycleEntities());
        }
        
        private void Cancel()
        {
            if (_routine != null)
            {
                ServerMgr.Instance.StopCoroutine(_routine);
                _routine = null;
            }
            
            _running = false;
        }
        
        private void GatherEntities()
        {
            foreach (BaseNetworkable nEntity in BaseNetworkable.serverEntities)
            {
                if (nEntity is TreeEntity { IsDestroyed: false, networkEntityScale: true } entity)
                    Enqueue(entity);
            }
        }
        
        private IEnumerator CycleEntities()
        {
            while (true)
            {
                yield return CoroutineEx.waitForSeconds(TICK_INTERVAL);
                
                _stopwatch.Restart();
                
                for (int i = _entities.Count - 1; i >= 0; i--)
                {
                    BaseEntity entity = _entities[i];
                    if (entity?.transform == null || entity.IsDestroyed || !entity.networkEntityScale)
                    {
                        _entities.RemoveAt(i);
                        continue;
                    }

                    if (entity.transform.localScale.y < FullScale.y)
                    {
                        entity.transform.localScale += GrowableManager.GrowScale;
                        entity.SendNetworkUpdateImmediate();
                    }
                    else
                    {
                        if (entity is TreeEntity { networkEntityScale: true } treeEntity)
                        {
                            treeEntity.spawnTreeAddition = true;
                            treeEntity.Invoke("TryAddTreeAddition", 0.25f);
                        }
                        
                        entity.networkEntityScale = false;
                        entity.transform.localScale = Vector3.one;
                        entity.SendNetworkUpdateImmediate();
                    }

                    entity.SendNetworkUpdateImmediate();

                    if (_stopwatch.Elapsed.TotalMilliseconds >= GrowableEntity.framebudgetms)
                        yield return CoroutineEx.waitForEndOfFrame;
                }
            }
        }
    }

    private void DeployableVolume()
    {
        _deployVolumes = PrefabAttribute.server.FindAll<DeployVolume>(1502013593);
    }
    
    private readonly struct PlantableClone
    {
        public readonly string Shortname;
        public readonly string Name;
        public readonly string Text;
        public readonly ulong Skin;

        public PlantableClone(Item item)
        {
            Shortname = item.info.shortname;
            Name = item.name;
            Text = item.text;
            Skin = item.skin;
        }
    }
    
    private readonly struct CanPlantResult
    {
        public readonly bool Success;
        public readonly string Reason;

        private CanPlantResult(bool success, string reason)
        {
            Success = success;
            Reason  = reason;
        }

        public static CanPlantResult Ok() => new(true,  string.Empty);
        
        public static CanPlantResult Failed(string reason) => new(false, reason);
    }
    
    private CanPlantResult CanPlantTree(BasePlayer player, GrowableEntity entity)
    {
        if (entity.GetParentEntity() is PlanterBox)
            return CanPlantResult.Failed(GetMessage(LangKeys.ERROR_GROUND_REQUIRED, player.UserIDString));
        
        if (!player.IsBuildingAuthed())
            return CanPlantResult.Failed(GetMessage(LangKeys.ERROR_MISSING_PRIVILEGE, player.UserIDString));
        
        Vector3 pos = entity.transform.position;
        if (AntiHack.IsInsideMesh(pos) || AntiHack.TestInsideTerrain(pos))
            return CanPlantResult.Failed(GetMessage(LangKeys.ERROR_INSIDE_OBJECT, player.UserIDString));
        
        if (_deployVolumes == null || _deployVolumes.Length == 0)
            return CanPlantResult.Ok();
        
        foreach (DeployVolume volume in _deployVolumes)
        {
            if (DeployVolume.CheckSphere(pos, 1f, PLANT_LAYER_MASK, volume))
                return CanPlantResult.Failed(GetMessage(LangKeys.ERROR_INSIDE_OBJECT, player.UserIDString));
        }

        return CanPlantResult.Ok();
    }
    
    private void TryPlantTree(BasePlayer player, GrowableEntity entity, TreeConfig treeConfig)
    {
        if (player == null || entity == null || treeConfig == null) 
            return;
        
        CanPlantResult plantResult = CanPlantTree(player, entity);
        if (plantResult.Success)
        {
            CreateEntity(player, entity.ServerWorldPosition, treeConfig.Prefab);
            RemoveEntity(entity);
            MessagePlayer(player, GetMessage(LangKeys.MESSAGE_PLANTED, player.UserIDString, treeConfig.DisplayName));
            return;
        }
        
        treeConfig.GiveItem(player, 1);
        RemoveEntity(entity);
        MessagePlayer(player, plantResult.Reason);
    }

    private void TryPlantSeed(BasePlayer player, GrowableEntity entity, PlantableClone plantableClone)
    {
        bool inPlanterBox = entity?.GetParentEntity() is PlanterBox;
        bool planterRequired = IsPlanterRequired(plantableClone.Shortname);
        if (inPlanterBox || !planterRequired)
            return;
        
        RefundEntity(player, plantableClone, entity?.Genes);
        RemoveEntity(entity);
        MessagePlayer(player, GetMessage(LangKeys.ERROR_PLANTER_REQUIRED, player.UserIDString));
    }
    
    private void HandlePlant(BasePlayer player, GrowableEntity entity, PlantableClone plantableClone)
    {
        try
        {
            TreeConfig? treeConfig = _configData.FindTreeConfig(plantableClone.Name);
            if (treeConfig != null && entity != null)
            {
                TryPlantTree(player, entity, treeConfig);
                return;
            }
            
            TryPlantSeed(player, entity, plantableClone);
        }
        catch (Exception e)
        {
            Puts("{0} | {1}", e.Message, e.StackTrace);
        }
    }
    
    private void CreateEntity(BasePlayer player, Vector3 position, string prefabName)
    {
        BaseEntity baseEntity = GameManager.server.CreateEntity(prefabName, position, Quaternion.identity);
        if (baseEntity == null)
        {
            Interface.Oxide.LogError("TreePlanter::CreateEntity: failed to create entity, invalid prefab path: {0}", prefabName);
            return;
        }

        baseEntity.EnableSaving(true);
        baseEntity.OwnerID = player.userID;
        
        if (_configData.EnableGrowing && baseEntity is TreeEntity { IsDestroyed: false } treeEntity)
        {
            treeEntity.spawnTreeAddition = false;
            treeEntity.networkEntityScale = true;
            treeEntity.transform.localScale = GrowableManager.GrowScale; 
        }
        
        baseEntity.Spawn();
        baseEntity.SendNetworkUpdateImmediate();
        
        if (!_configData.EnableGrowing) 
            return;
        
        GrowableManager.Instance.Enqueue(baseEntity);
    }

    private static void RemoveEntity(GrowableEntity entity, float delay = 0.25f)
    {
        if (entity != null && !entity.IsDestroyed) 
            entity.Invoke(entity.AdminKill, delay);
    }

    private static void RefundEntity(BasePlayer player, PlantableClone clone, GrowableGenes? genes)
    {
        Item item = ItemManager.CreateByName(clone.Shortname, 1, clone.Skin);
        if (!string.IsNullOrEmpty(clone.Name)) item.name = clone.Name;
        if (!string.IsNullOrEmpty(clone.Text)) item.text = clone.Text;
        
        if (genes != null)
        {
            item.instanceData = new ProtoBuf.Item.InstanceData();
            item.instanceData.dataInt = GrowableGeneEncoding.EncodeGenesToInt(genes);                    
        }
                
        item.MarkDirty();

        if (player.inventory != null && player.inventory.GiveItem(item))
            return;
        
        item.DropAndTossUpwards(player.GetDropPosition(), 0.2f);
    }
    
    #endregion

    #region UI

    private const string UI_PLANT_CONTAINER = "UI_PLANT_CONTAINER";
    private const string UI_POPUP_CONTAINER = "UI_POPUP_CONTAINER";
    private const string UI_HEADING_CLOSE = "UI_HEADING_CLOSE";
    private const string UI_HEADING_LABEL = "UI_HEADING_LABEL";
    private const string UI_ITEMS_CONTAINER = "UI_ITEMS_CONTAINER";
    private const string UI_PREV_BUTTON = "UI_PREV_BUTTON";
    private const string UI_NEXT_BUTTON = "UI_NEXT_BUTTON";
    private const string UI_PURCHASE_CMD = "treeplanter.purchase {0}";
    private const string UI_PAGINATE_CMD = "treeplanter.page {0}";
    
    private const int UI_ITEMS_PER_PAGE = 72;

    private void CacheImages()
    {
        if (ImageLibrary is not { IsLoaded: true })
            return;
        
        Dictionary<string, string> images = new Dictionary<string, string>();
        
        foreach (TreeConfig treeConfig in _configData.TreeConfigs)
        {
            if (!string.IsNullOrEmpty(treeConfig.ImageUrl)) 
                images.Add(treeConfig.Name, treeConfig.ImageUrl);
        }

        if (images.Count == 0)
            return;
        
        ImageLibrary.Call("ImportImageList", Title, images, 0UL, true);
    }
    
    private void CachedMenuUI()
    {
        CuiElementContainer container = new CuiElementContainer();
            
        UiBuilder.AddPanel(container, parentName: "Overlay", 
            elementName: UI_PLANT_CONTAINER, 
            color: UiBuilder.Color3, 
            anchorMin: "0 0", 
            anchorMax: "1 1", 
            cursorEnabled: true, 
            keyboardEnabled: true);
            
        UiBuilder.AddPanel(container, parentName: UI_PLANT_CONTAINER, 
            color: UiBuilder.Color4, 
            anchorMin: "0 0.94", 
            anchorMax: "1 1");
            
        _menuLayout = container;
        _totalPages = Math.Max(1, _configData.TreeConfigs.Count - 1) / UI_ITEMS_PER_PAGE;
    }
    
    private void DestroyMenuUI()
    {
        _menuPopup.Clear();
        _menuPages.Clear();
        
        _menuLayout = null;
        _menuGrid = null;
        
        UiBuilder.DestroyUi(Network.Net.sv?.connections, UI_POPUP_CONTAINER, UI_PLANT_CONTAINER);
    }
    
    private void DisplayMenuUI(BasePlayer player, int currentPage, bool isFirst = false)
    {
        _menuPages[player.userID] = currentPage;
        
        CuiElementContainer container = new CuiElementContainer();
        if (isFirst) CreateStaticElements(container, player);
        
        CreateItemsContainer(container);
        CreateItemsGrid(container, currentPage, player);
        UiBuilder.AddUi(container, player);
    }
    
    private void DestroyMenuUI(BasePlayer player)
    {
        if (player == null) 
            return;
        
        _menuPages.Remove(player.userID);
        CuiHelper.DestroyUi(player, UI_PLANT_CONTAINER);
    }
    
    private void DisplayPopupUI(BasePlayer player, UiBuilder.PopupStyle style, string message, params object[] args)
    {
        const string popupAnchorMin = "0.18 0.88";
        const string popupAnchorMax = "0.798 0.92";
            
        CuiElementContainer container = new CuiElementContainer();
        ulong userID = player.userID;
        
        UiBuilder.AddButton(container, parentName: "Overlay",
            elementName: UI_POPUP_CONTAINER, 
            fontBold: true, 
            text: (args?.Length > 0 ? string.Format(message, args) : message), 
            textColor: UiBuilder.Color2, 
            buttonColor: UiBuilder.GetPopupColor(style), 
            anchorMin: popupAnchorMin, 
            anchorMax: popupAnchorMax, 
            close: UI_POPUP_CONTAINER, 
            destroyUi: true);
        
        UiBuilder.AddUi(container, player);
        
        if (_menuPopup.TryGetValue(userID, out Timer popupTimer) && !popupTimer.Destroyed)
            popupTimer.DestroyToPool();
        
        _menuPopup[userID] = timer.Once(2.5f, () => DestroyPopupUI(player, userID));
    }
    
    private void DestroyPopupUI(BasePlayer player, ulong userID)
    {
        _menuPopup.Remove(userID);
        CuiHelper.DestroyUi(player, UI_POPUP_CONTAINER);
    }
    
    private void CreateStaticElements(CuiElementContainer container, BasePlayer player)
    {
        const string labelAnchorMin = "0 0.94";
        const string labelAnchorMax = "1 1";
        const string closeAnchorMin = "0.9 0.94";
        const string closeAnchorMax = "1 1";
        const string nextAnchorMin = "0.505 0.02";
        const string nextAnchorMax = "0.7 0.08";
        const string prevAnchorMin = "0.3 0.02";
        const string prevAnchorMax = "0.495 0.08";
            
        CuiHelper.AddUi(player, _menuLayout);
                
        UiBuilder.AddText(container, parentName: UI_PLANT_CONTAINER,
            elementName: UI_HEADING_LABEL,
            fontBold: true, 
            text: GetMessage(LangKeys.UI_HEADING_TEXT, player.UserIDString), 
            textColor: UiBuilder.Color2, 
            textAnchor: TextAnchor.MiddleCenter, 
            anchorMin: labelAnchorMin, 
            anchorMax: labelAnchorMax, 
            destroyUi: true);
                
        UiBuilder.AddButton(container, parentName: UI_PLANT_CONTAINER, 
            elementName: UI_HEADING_CLOSE,
            fontBold: true, 
            text: GetMessage(LangKeys.UI_CLOSE_TEXT, player.UserIDString), 
            textColor: UiBuilder.Color1,
            buttonColor: UiBuilder.Color5, 
            anchorMin: closeAnchorMin, 
            anchorMax: closeAnchorMax, 
            close: UI_PLANT_CONTAINER, 
            command: "treeplanter.close",
            destroyUi: true);
            
        UiBuilder.AddButton(container, parentName: UI_PLANT_CONTAINER, 
            elementName: UI_PREV_BUTTON, 
            fontBold: true, 
            text: GetMessage(LangKeys.UI_PREV_ICON, player.UserIDString), 
            textColor: UiBuilder.Color1,
            buttonColor: UiBuilder.Color4, 
            anchorMin: prevAnchorMin, 
            anchorMax: prevAnchorMax, 
            command: string.Format(UI_PAGINATE_CMD, "prev"),
            destroyUi: true);
            
        UiBuilder.AddButton(container, parentName: UI_PLANT_CONTAINER, 
            elementName: UI_NEXT_BUTTON, 
            fontBold: true, 
            text: GetMessage(LangKeys.UI_NEXT_ICON, player.UserIDString), 
            textColor: UiBuilder.Color1,
            buttonColor: UiBuilder.Color4, 
            anchorMin: nextAnchorMin, 
            anchorMax: nextAnchorMax,  
            command: string.Format(UI_PAGINATE_CMD, "next"),
            destroyUi: true);
    }
    
    private void CreateItemsContainer(CuiElementContainer container)
    {
        const string itemAnchorMin = "0 0.1";
        const string itemAnchorMax = "1 0.93";
            
        UiBuilder.AddPanel(container, parentName: UI_PLANT_CONTAINER, 
            elementName: UI_ITEMS_CONTAINER, 
            color: UiBuilder.Color7, 
            anchorMin: itemAnchorMin, 
            anchorMax: itemAnchorMax, 
            destroyUi: true);
    }
    
    private void CreateItemsGrid(CuiElementContainer container, int currentPage, BasePlayer player)
    {
        const string textAnchorMin = "0 0";
        const string textAnchorMax = "1 1";
        
        int totalItems = _configData.TreeConfigs.Count;
        if (totalItems == 0)
        {
            UiBuilder.AddText(container, parentName: UI_ITEMS_CONTAINER,
                fontBold: true, 
                text: GetMessage(LangKeys.UI_EMPTY_TEXT, player.UserIDString), 
                textColor: UiBuilder.Color2, 
                textAnchor: TextAnchor.MiddleCenter, 
                anchorMin: textAnchorMin, 
                anchorMax: textAnchorMax);
        }
        else
        {
            int limit = Mathf.Min(totalItems, (currentPage + 1) * UI_ITEMS_PER_PAGE);
            int index = 0;
                
            for (int i = currentPage * UI_ITEMS_PER_PAGE; i < limit; i++)
            {
                UiAnchor uiPosition = _menuGrid.GetAnchor(index);
                TreeConfig treeConfig = _configData.TreeConfigs[i];
                bool hasIcon = !string.IsNullOrEmpty(treeConfig.ImageUrl) || treeConfig.Skin != 0UL;
                string[] iconAnchors = uiPosition.Icon;
                string[] textAnchors = hasIcon ? uiPosition.Text : uiPosition.Full;
                
                if (hasIcon)
                {
                    if (treeConfig.Skin != 0UL)
                    {
                        UiBuilder.AddIcon(container,
                            parentName: UI_ITEMS_CONTAINER, 
                            itemID: ICON_ITEM_ID,
                            skinID: treeConfig.Skin,
                            anchorMin: iconAnchors[0], 
                            anchorMax: iconAnchors[1]);
                    }
                    else
                    {
                        UiBuilder.AddImage(container, 
                            parentName: UI_ITEMS_CONTAINER, 
                            png: ImageLibrary?.Call<string>("GetImage", treeConfig.Name, 0UL, true),
                            anchorMin: iconAnchors[0], 
                            anchorMax: iconAnchors[1]);
                    }
                }
                    
                UiBuilder.AddButton(container, parentName: UI_ITEMS_CONTAINER,
                    fontBold: true, 
                    fontSize: 10,
                    text: GetMessage(LangKeys.UI_ITEMS_TEXT, player.UserIDString, treeConfig.DisplayName, treeConfig.Cost, treeConfig.Amount), 
                    textColor: UiBuilder.Color1,
                    buttonColor: UiBuilder.Color4,
                    command: string.Format(UI_PURCHASE_CMD, treeConfig.Name),
                    anchorMin: textAnchors[0], 
                    anchorMax: textAnchors[1]);
                    
                ++index;
            }
        }
    }
    
    private class UiGrid
    {
        private const int COLS = 9;
        private const float WIDTH = 0.1f;
        private const float HEIGHT = 0.1f;
        private const float X_OFFSET = 0.01f;
        private const float Y_OFFSET = 0.92f;
        private const float X_SPACING = 0.01f;
        private const float Y_SPACING = 0.0172f;
        private readonly Dictionary<int, UiAnchor> _anchors = new();
        
        public UiAnchor GetAnchor(int index)
        {
            if (_anchors.TryGetValue(index, out UiAnchor anchors))
                return anchors;
            
            int row = index / COLS;
            int col = index % COLS;
            float xStart = X_OFFSET + col * (WIDTH + X_SPACING);
            float yTop = Y_OFFSET - row * (HEIGHT + Y_SPACING);
            float iconWidth = WIDTH * 0.3f;
            float spacing = WIDTH * 0.05f;
            float textWidth = WIDTH - iconWidth - spacing;
            
            _anchors[index] = anchors = new UiAnchor
            {
                Icon = new[] { $"{xStart} {(yTop - HEIGHT)}", $"{(xStart + iconWidth)} {yTop}" },
                Text = new[] { $"{(xStart + iconWidth + spacing)} {(yTop - HEIGHT)}", $"{(xStart + iconWidth + spacing + textWidth)} {yTop}" },
                Full = new[] { $"{xStart} {(yTop - HEIGHT)}", $"{(xStart + WIDTH)} {yTop}" },
            };
            
            return anchors;
        }
    }
    
    private class UiAnchor
    {
        public string[] Icon;
        public string[] Text;
        public string[] Full;
    }

    private static class UiBuilder
    {
        public const string Color1 = "1 1 1 1";             // Pure white
        public const string Color2 = "0.91 0.87 0.83 1";    // Light paper tone
        public const string Color3 = "0.145 0.135 0.12 1";  // Deep charcoal
        public const string Color4 = "0.187 0.179 0.172 1"; // Card/Panel surface
        public const string Color5 = "0.8 0.28 0.2 1";      // Alert/CTA
        public const string Color6 = "0.23 0.46 0.31 1";    // Confirm/Success
        public const string Color7 = "0 0 0 0";             // Clear/None
        public const string Color8 = "0.25 0.50 0.75 0.7";  // Selection/Hover effect

        public enum PopupStyle { Error, Info, Success }
            
        public static string GetPopupColor(PopupStyle style)
        {
            return style switch
            {
                PopupStyle.Success => Color6,
                PopupStyle.Error => Color5,
                PopupStyle.Info => Color4,
                _ => Color2
            };
        }
            
        public static void AddPanel(CuiElementContainer container, 
            string parentName, 
            string? elementName = null, 
            string color = "1 1 1 1",
            string? sprite = null,
            string material = "assets/content/ui/namefontmaterial.mat",
            Image.Type imageType = Image.Type.Tiled,
            string anchorMin = "0 0",
            string anchorMax = "1 1",
            string? offsetMin = null,
            string? offsetMax = null,
            float fadeOut = 0f, 
            float fadeIn = 0f, 
            bool destroyUi = false,
            bool cursorEnabled = false,
            bool keyboardEnabled = false) 
        {
            container.Add(new CuiPanel
            {
                Image =
                {
                    Color = color,
                    Sprite = sprite,
                    Material = material,
                    ImageType = imageType,
                    FadeIn = fadeIn
                },
                RectTransform =
                {
                    AnchorMin = anchorMin, AnchorMax = anchorMax,
                    OffsetMin = offsetMin, OffsetMax = offsetMax,
                },
                FadeOut = fadeOut,
                CursorEnabled = cursorEnabled,
                KeyboardEnabled = keyboardEnabled
            }, parentName, elementName, (destroyUi ? elementName : null));
        }

        public static void AddText(CuiElementContainer container, 
            string parentName, 
            string? elementName = null, 
            string textColor = "0 0 0 1",
            string text = "",
            TextAnchor textAnchor = TextAnchor.MiddleCenter, 
            int fontSize = 14,
            bool fontBold = false,
            string anchorMin = "0 0", 
            string anchorMax = "1 1",
            string? offsetMin = null, 
            string? offsetMax = null, 
            bool destroyUi = false) 
        {
            container.Add(new CuiElement
            {
                Parent = parentName,
                Name = elementName,
                DestroyUi = (destroyUi ? elementName : null),
                Components =
                {
                    new CuiTextComponent
                    {
                        Font = (fontBold ? "RobotoCondensed-Bold.ttf" : "RobotoCondensed-Regular.ttf"),
                        FontSize = fontSize,
                        Text = text,
                        Color = textColor,
                        Align = textAnchor,
                    },
                    new CuiRectTransformComponent
                    {
                        AnchorMin = anchorMin, AnchorMax = anchorMax,
                        OffsetMin = offsetMin, OffsetMax = offsetMax,
                    }
                }
            });
        }

        public static void AddButton(CuiElementContainer container, 
            string parentName, 
            string? elementName = null,
            string text = "",
            string textColor = "0 0 0 1",
            TextAnchor textAnchor = TextAnchor.MiddleCenter, 
            int fontSize = 12,
            bool fontBold = true,
            string buttonColor = "1 1 1 1", 
            string? command = null,
            string? close = null,
            string? sprite = null,
            string material = "assets/content/ui/namefontmaterial.mat",
            Image.Type imageType = Image.Type.Tiled,
            string anchorMin = "0 0", 
            string anchorMax = "1 1",
            string? offsetMin = null, 
            string? offsetMax = null,
            bool destroyUi = false) 
        {
            container.Add(new CuiButton
            {
                Text =
                {
                    Font = (fontBold ? "RobotoCondensed-Bold.ttf" : "RobotoCondensed-Regular.ttf"),
                    FontSize = fontSize,
                    Text = text,
                    Color = textColor,
                    Align = textAnchor,
                },
                Button =
                {
                    Color = buttonColor,
                    Close = close,
                    Command = command,
                    Sprite = sprite,
                    Material = material, 
                    ImageType = imageType,
                },
                RectTransform =
                {
                    AnchorMin = anchorMin, AnchorMax = anchorMax,
                    OffsetMin = offsetMin, OffsetMax = offsetMax,
                }
            }, parentName, elementName, (destroyUi ? elementName : null));
        }
            
        public static void AddImage(CuiElementContainer container, 
            string parentName, 
            string? elementName = null,
            string? png = null, 
            string? url = null, 
            string anchorMin = "0 0", 
            string anchorMax = "1 1",
            string? offsetMin = null, 
            string? offsetMax = null,
            float fadeIn = 0.5f,
            bool destroyUi = false) 
        {
            if (!string.IsNullOrEmpty(url))
            {
                container.Add(new CuiElement 
                {
                    Parent = parentName,
                    Name = elementName,
                    DestroyUi = (destroyUi ? elementName : null),
                    Components =
                    {
                        new CuiRawImageComponent
                        {
                            Url = url,
                            FadeIn = fadeIn,
                        },
                        new CuiRectTransformComponent
                        {
                            AnchorMin = anchorMin, AnchorMax = anchorMax,
                            OffsetMin = offsetMin, OffsetMax = offsetMax
                        }
                    }
                });
                    
                return;
            }
                
            if (!string.IsNullOrEmpty(png))
            {
                container.Add(new CuiElement 
                {
                    Parent = parentName,
                    Name = elementName,
                    DestroyUi = (destroyUi ? elementName : null),
                    Components =
                    {
                        new CuiRawImageComponent
                        {
                            Png = png,
                            FadeIn = fadeIn,
                        },
                        new CuiRectTransformComponent
                        {
                            AnchorMin = anchorMin, AnchorMax = anchorMax,
                            OffsetMin = offsetMin, OffsetMax = offsetMax
                        }
                    }
                });
            }
        }
            
        public static void AddIcon(CuiElementContainer container, 
            string parentName, 
            string? elementName = null,
            int itemID = 0,
            ulong skinID = 0UL,
            string imageColor = "0 0 0 0", 
            string? material = null,
            string anchorMin = "0 0", 
            string anchorMax = "1 1",
            string? offsetMin = null, 
            string? offsetMax = null,
            float fadeIn = 0.5f,
            bool destroyUi = false) 
        {
            container.Add(new CuiElement
            {
                Parent = parentName,
                Name = elementName,
                DestroyUi = (destroyUi ? elementName : null),
                Components =
                {
                    new CuiImageComponent
                    {
                        Color = imageColor,
                        ItemId = itemID,
                        SkinId = skinID,
                        FadeIn = fadeIn,
                        Material = material,
                    },
                    new CuiRectTransformComponent
                    {
                        AnchorMin = anchorMin, AnchorMax = anchorMax,
                        OffsetMin = offsetMin, OffsetMax = offsetMax
                    }
                }
            });
        }
            
        public static void AddUi(CuiElementContainer container, BasePlayer player) 
        {
            try
            {
                string json = CuiHelper.ToJson(container);
                CuiHelper.AddUi(player, json);
                
                foreach (CuiElement element in container)
                    element.Components?.Clear();
            }
            finally
            {
                container.Clear();
            }
        }
        
        public static void AddUi(List<Network.Connection>? connections, CuiElementContainer elements)
        {
            if (connections == null || connections.Count == 0) 
                return;            
            
            try
            {
                string json = CuiHelper.ToJson(elements);
                CommunityEntity.ServerInstance.ClientRPC<string>(RpcTarget.Players("AddUI", connections), json);
            }
            finally
            {
                elements.Clear();
            }
        }

        public static void DestroyUi(List<Network.Connection>? connections, params string[]? elementNames)
        {
            if (connections == null || connections.Count == 0) 
                return;
            
            if (elementNames == null)
                return;
            
            try
            {
                foreach (string elementName in elementNames)
                    CommunityEntity.ServerInstance.ClientRPC<string>(RpcTarget.Players("DestroyUI", connections), elementName);
            }
            catch
            {
                //
            }
        }
    }
        
    #endregion
    
    #region Chat Command
    
    private void ListCommand(BasePlayer player, int chunkSize = 25)
    {
        StringBuilder sb = Facepunch.Pool.Get<StringBuilder>();
        sb.Clear();
        
        try
        {
            sb.Append(GetMessage(LangKeys.MESSAGE_PREFIX, player.UserIDString));
            sb.Append(GetMessage(LangKeys.MESSAGE_INFO, player.UserIDString));
            
            MessagePlayer(player, sb.ToString());
            
            sb.Clear();
            
            int treeCount = _configData.TreeConfigs.Count;
            
            for (int i = 0; i < treeCount; i++)
            {
                if (sb.Length > 0)
                    sb.Append(" | ");
                
                TreeConfig treeConfig = _configData.TreeConfigs[i];
                sb.Append(GetMessage(LangKeys.MESSAGE_ITEM, player.UserIDString, treeConfig.DisplayName, treeConfig.Cost));
                
                if (i % chunkSize == 0)
                {
                    MessagePlayer(player, sb.ToString());
                    sb.Clear();
                }
            }

            if (sb.Length > 0)
            {
                MessagePlayer(player, sb.ToString());
                sb.Clear();
            }
        }
        finally
        {
            sb.Clear();
            Facepunch.Pool.FreeUnmanaged(ref sb);
        }
    }    

    [ChatCommand("tree")]
    private void TreeCommand(BasePlayer player, string command, string[] args)
    {
        if (!HasPermission(player, PERM_USE))
        {
            MessagePlayer(player, GetMessage(LangKeys.ERROR_NO_PERMISSION, player.UserIDString));
            return;
        }
        
        if (args.Length == 0)
        {
            if (HasPermission(player, PERM_MENU))
            {
                _menuPages.TryGetValue(player.userID, out int currentPage);
                
                DisplayMenuUI(player, currentPage, true);
            }
            else
            {
                ListCommand(player);
            }
                
            return;
        }
        
        StringBuilder sb = Facepunch.Pool.Get<StringBuilder>();
        sb.Clear();
        
        try
        {
            string name = FormattedName(sb, args);
            if (string.IsNullOrEmpty(name))
            {
                MessagePlayer(player, GetMessage(LangKeys.ERROR_MISSING_ITEM, player.UserIDString));
                return;
            }
            
            TreeConfig? treeConfig = _configData.FindTreeConfig(name);
            if (treeConfig == null)
            {
                ListCommand(player);
                MessagePlayer(player, GetMessage(LangKeys.ERROR_MISSING_ITEM, player.UserIDString));
                return;
            }
            
            if (!IsValidMultiplier(args[^1], out int multiplier)) 
                multiplier = 1;
            
            if (!TakeCurrency(player, treeConfig.Cost * multiplier))
            {
                MessagePlayer(player, GetMessage(LangKeys.ERROR_MISSING_BALANCE, player.UserIDString));
                return;
            }
            
            int amount = treeConfig.Amount * multiplier;
            treeConfig.GiveItem(player, amount);
            MessagePlayer(player, GetMessage(LangKeys.MESSAGE_RECEIVED, player.UserIDString, amount, treeConfig.DisplayName));
        }
        finally
        {
            sb.Clear();
            Facepunch.Pool.FreeUnmanaged(ref sb);
        }
    }

    #endregion

    #region Console Command
    
    [ConsoleCommand("treeplanter.show")]
    private void ShowConsole(ConsoleSystem.Arg arg)
    {
        BasePlayer player = arg.Player();
        if (player == null || !HasPermission(player, PERM_MENU))
            return;
            
        if (!arg.HasArgs())
            return;
        
        DisplayMenuUI(player, 0, true);
    }
    
    [ConsoleCommand("treeplanter.close")]
    private void CloseConsole(ConsoleSystem.Arg arg)
    {
        BasePlayer player = arg.Player();
        if (player == null)
            return;

        DestroyMenuUI(player);
    }

    [ConsoleCommand("treeplanter.page")]
    private void TreeConsole(ConsoleSystem.Arg arg)
    {
        BasePlayer player = arg.Player();
        if (player == null || !HasPermission(player, PERM_MENU))
            return;
            
        if (!arg.HasArgs())
            return;

        _menuPages.TryGetValue(player.userID, out int currentPage);

        currentPage = arg.GetString(0) switch
        {
            "prev" => Math.Clamp((currentPage - 1), 0, _totalPages),
            "next" => Math.Clamp((currentPage + 1), 0, _totalPages),
            _ => currentPage
        };

        DisplayMenuUI(player, currentPage, false);
    }
        
    [ConsoleCommand("treeplanter.purchase")]
    private void TreePurchase(ConsoleSystem.Arg arg)
    {
        BasePlayer player = arg.Player();
        if (player == null || !HasPermission(player, PERM_MENU))
            return;
            
        if (!arg.HasArgs())
            return;

        TreeConfig? treeConfig = _configData.FindTreeConfig(arg.GetString(0));
        if (treeConfig == null)
        {
            DisplayPopupUI(player, UiBuilder.PopupStyle.Error, GetMessage(LangKeys.ERROR_MISSING_ITEM, player.UserIDString));
            return;
        }

        int multiplier = arg.GetInt(1, 1);
        multiplier = Math.Clamp(multiplier, 1, int.MaxValue);
        if (!TakeCurrency(player, treeConfig.Cost * multiplier))
        {
            DisplayPopupUI(player, UiBuilder.PopupStyle.Error, GetMessage(LangKeys.ERROR_MISSING_BALANCE, player.UserIDString));
            return;
        }

        int amount = treeConfig.Amount * multiplier;
        treeConfig.GiveItem(player, amount);
        DisplayPopupUI(player, UiBuilder.PopupStyle.Success, GetMessage(LangKeys.MESSAGE_RECEIVED, player.UserIDString, amount, treeConfig.DisplayName));
    }

    #endregion

    #region Helpers

    private bool HasPermission(BasePlayer player, string permName)
    {
        return permission.UserHasPermission(player.UserIDString, permName);
    }

    private bool SameClan(ulong userID, ulong ownerID)
    {
        string? playerClan = Clans?.Call<string>("GetClanOf", userID);
        if (string.IsNullOrEmpty(playerClan)) 
            return false;
        
        string? targetClan = Clans?.Call<string>("GetClanOf", ownerID);
        if (string.IsNullOrEmpty(targetClan)) 
            return false;
        
        return playerClan == targetClan;
    }
        
    private bool SameTeam(ulong userID, ulong ownerID)
    {
        RelationshipManager.PlayerTeam? playerTeam = RelationshipManager.ServerInstance?.FindPlayersTeam(ownerID);
        return playerTeam != null && playerTeam.members.Contains(userID);
    }
    
    private Item GetOwnerItem(Planner planner)
    {
        BasePlayer player = planner.GetOwnerPlayer();
        return player == null || player.inventory == null ? (Item) null : player.inventory.FindItemByUID(planner.ownerItemUID);
    }
    
    private bool TakeCurrency(BasePlayer player, int cost)
    {
        if (cost == 0)
            return true;

        if (_configData.UseServerRewards && ServerRewards is { IsLoaded: true })
        {
            if (ServerRewards.Call<object>("TakePoints", player.userID, cost) != null)
                return true;
        }

        if (_configData.UseEconomics && Economics is { IsLoaded: true })
        {
            if (Economics.Call<bool>("Withdraw", player.userID, (double)cost))
                return true;
        }

        if (_configData.UseCurrencyItem && HasAmount(player, cost))
        {
            TakeAmount(player, cost);
            return true;
        }

        return false;
    }

    private bool IsEntityOwner(ulong userID, ulong ownerID)
    {
        if (userID == 0UL || ownerID == 0UL) return false;
        if (_configData.EnableOwner && userID == ownerID) return true;
        if (_configData.EnableTeam && SameTeam(userID, ownerID)) return true;
        if (_configData.EnableClan && SameClan(userID, ownerID)) return true;
        return false;
    }
    
    private bool IsPlanterRequired(string shortname)
    {
        _configData.AgriBlocked.TryGetValue(shortname, out bool blockedAgri);
        return blockedAgri;
    }

    private bool IsValidMultiplier(string arg, out int multiplier)
    {
        return int.TryParse(arg, out multiplier) && multiplier > 0;
    }
    
    private static string FormattedName(StringBuilder sb, string[] args)
    {
        if (args.Length == 0) 
            return string.Empty;
        
        bool addedSegment = false;
        
        for (int i = 0; i < args.Length; i++)
        {
            string part = args[i];
            if (string.IsNullOrEmpty(part))
                continue;
            
            int lengthBefore = sb.Length;
            
            foreach (char c in part)
            {
                if (char.IsLetter(c))
                    sb.Append(c);
            }

            bool segmentAddedChars = sb.Length > lengthBefore;
            if (segmentAddedChars)
            {
                if (addedSegment)
                    sb.Insert(lengthBefore, ' ');
                
                addedSegment = true;
            }
        }

        return sb.ToString();
    }
        
    #endregion

    #region Inventory Methods | Needed To Check For Skinned Scrap

    private bool HasAmount(BasePlayer player, int amount) => GetAmount(player.inventory, _configData.CurrencyItemID, _configData.CurrencySkinID) >= amount;

    private void TakeAmount(BasePlayer player, int amount) => TakeAmount(player.inventory, _configData.CurrencyItemID, _configData.CurrencySkinID, amount);

    private static int GetAmount(PlayerInventory inventory, int itemId, ulong skinId = 0UL)
    {
        if (itemId == 0) 
            return 0;

        int totalAmount = GetAmountFromContainer(inventory.containerMain, itemId, skinId);
        totalAmount += GetAmountFromContainer(inventory.containerBelt, itemId, skinId);
            
        return totalAmount;
    }

    private static int GetAmountFromContainer(ItemContainer container, int itemId, ulong skinId = 0UL, bool usable = false)
    {
        if (container?.itemList == null) 
            return 0;

        int num = 0;
        foreach (Item obj in container.itemList)
        {
            if (obj.info.itemid == itemId && obj.skin == skinId && (!usable || !obj.IsBusy()))
                num += obj.amount;
        }
            
        return num;
    }

    private static int TakeAmount(PlayerInventory inventory, int itemId, ulong skinId, int amount)
    {
        if (itemId == 0) 
            return 0;
        
        int totalTaken = TakeAmountFromContainer(inventory.containerMain, itemId, skinId, amount);
        if (totalTaken < amount) 
            totalTaken += TakeAmountFromContainer(inventory.containerBelt, itemId, skinId, amount - totalTaken);
        
        return totalTaken;
    }

    private static int TakeAmountFromContainer(ItemContainer container, int itemId, ulong skinId, int amount)
    {
        if (container?.itemList == null || amount <= 0) 
            return 0;

        List<Item> list = Facepunch.Pool.Get<List<Item>>();
        int totalTaken = 0;
        
        foreach (Item item in container.itemList)
        {
            if (totalTaken >= amount) 
                break;
            
            if (item.info.itemid != itemId || item.skin != skinId) 
                continue;
            
            int take = Math.Min(item.amount, amount - totalTaken);
            item.amount -= take;
            item.MarkDirty();
            totalTaken += take;

            if (item.amount == 0)
                list.Add(item);
        }

        foreach (Item item in list)
        {
            item.RemoveFromContainer();
            item.Remove();
        }

        Facepunch.Pool.FreeUnmanaged(ref list);
        return totalTaken;
    }

    #endregion
}