﻿/***********************************************************************************************************************/
/*** DO NOT edit this file! Edit the files under `oxide/config` and/or `oxide/lang`, created once plugin has loaded. ***/
/*** Please note, support cannot be provided if the plugin has been modified. Please use a fresh copy if modified.   ***/
/***********************************************************************************************************************/

#define DEBUG

using System.Collections.Generic;
using System.Linq;
using Facepunch;
using Newtonsoft.Json;
using Random = System.Random;

namespace Oxide.Plugins
{
    [Info("No Nudity", "Wulf", "2.0.0")]
    [Description("Forces players to wear clothing, and gives them some if they do not have any")]
    public class NoNudity : CovalencePlugin
    {
        #region Configuration

        private Configuration _config;

        public class Configuration
        {
            [JsonProperty("Force clothing to be worn on top")]
            public bool ForceTopItem = true;

            [JsonProperty("Force clothing to be worn on bottom")]
            public bool ForceBottomItem = true;

            [JsonProperty("Top clothing item shortnames and skin IDs", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public Dictionary<string, List<ulong>> TopItems = new Dictionary<string, List<ulong>> { { "tshirt", new List<ulong> { 826053201uL } } };

            [JsonProperty("Bottom clothing item shortnames and skin IDs", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public Dictionary<string, List<ulong>> BottomItems = new Dictionary<string, List<ulong>> { { "pants", new List<ulong> { 1213635270uL } } };

            public string ToJson() => JsonConvert.SerializeObject(this);

            public Dictionary<string, object> ToDictionary() => JsonConvert.DeserializeObject<Dictionary<string, object>>(ToJson());
        }

        protected override void LoadDefaultConfig() => _config = new Configuration();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<Configuration>();
                if (_config == null)
                {
                    throw new JsonException();
                }

                if (!_config.ToDictionary().Keys.SequenceEqual(Config.ToDictionary(x => x.Key, x => x.Value).Keys))
                {
                    LogWarning("Configuration appears to be outdated; updating and saving");
                    SaveConfig();
                }
            }
            catch
            {
                LogWarning($"Configuration file {Name}.json is invalid; using defaults");
                LoadDefaultConfig();
            }
        }

        protected override void SaveConfig()
        {
            LogWarning($"Configuration changes saved to {Name}.json");
            Config.WriteObject(_config, true);
        }

        #endregion Configuration

        #region Initialization

        private readonly Dictionary<ItemDefinition, ulong> _topItemDefs = new Dictionary<ItemDefinition, ulong>();
        private readonly Dictionary<ItemDefinition, ulong> _bottomItemDefs = new Dictionary<ItemDefinition, ulong>();
        private readonly Random _random = new Random();

        private const string PermissionAllowed = "nonudity.allowed";

        private void OnServerInitialized()
        {
            permission.RegisterPermission(PermissionAllowed, this);

            if (!_config.ForceTopItem && !_config.ForceBottomItem)
            {
                Unsubscribe(nameof(CanMoveItem));
                Unsubscribe(nameof(OnItemAction));
                Unsubscribe(nameof(OnPlayerConnected));
                Unsubscribe(nameof(OnPlayerRespawned));

                LogWarning("Both top and bottom items are not forced; this may cause unexpected nudity");
                return;
            }

            // Make a list of top item definitions based on configuration
            foreach (KeyValuePair<string, List<ulong>> items in _config.TopItems)
            {
                ItemDefinition itemDef = ItemManager.FindItemDefinition(items.Key);
                if (itemDef != null && IsWearableTop(itemDef))
                {
                    for (int i = 0; i < items.Value.Count; i++)
                    {
                        _topItemDefs.Add(itemDef, items.Value[i]);
                    }
                }
            }
            if (_topItemDefs.Count > 0)
            {
                Log($"{_topItemDefs.Count} top clothing item(s) loaded from configuraton");
            }
            else
            {
                LogWarning("No top clothing items found; this may cause unexpected nudity");
            }

            // Make a list of bottom item definitions based on configuration
            foreach (KeyValuePair<string, List<ulong>> items in _config.BottomItems)
            {
                ItemDefinition itemDef = ItemManager.FindItemDefinition(items.Key);
                if (itemDef != null && IsWearableBottom(itemDef))
                {
                    for (int i = 0; i < items.Value.Count; i++)
                    {
                        _bottomItemDefs.Add(itemDef, items.Value[i]);
                    }
                }
            }
            if (_bottomItemDefs.Count > 0)
            {
                Log($"{_bottomItemDefs.Count} bottom clothing item(s) loaded from configuraton");
            }
            else
            {
                LogWarning("No bottom clothing items found; this may cause unexpected nudity");
            }

            // Check connected players for nudity
            for (int i = 0; i < BasePlayer.activePlayerList.Count; i++)
            {
                CheckAttire(BasePlayer.activePlayerList[i]);
            }
        }

        private void OnPlayerConnected(BasePlayer player) => CheckAttire(player);

        private void OnPlayerRespawned(BasePlayer player) => CheckAttire(player);

        #endregion Initialization

        #region Item Handling

        private void CheckAttire(BasePlayer player)
        {
            if (permission.UserHasPermission(PermissionAllowed, player.UserIDString))
            {
                return;
            }

            List<Item> availableItems = Pool.GetList<Item>();
            availableItems.AddRange(player.inventory.containerMain.itemList);
            availableItems.AddRange(player.inventory.containerBelt.itemList);

            List<Item> availableTops = Pool.GetList<Item>();
            List<Item> availableBottoms = Pool.GetList<Item>();

            // Make a list of clothing items in player's inventory
            foreach (Item item in availableItems)
            {
                if (IsWearableTop(item.info))
                {
                    availableTops.Add(item);
                }
                if (IsWearableBottom(item.info))
                {
                    availableBottoms.Add(item);
                }
            }

            bool wearingTop = false;
            bool wearingBottom = false;

            // Make a list existing items player is wearing
            List<Item> wornItems = player.inventory.containerWear.itemList;
            foreach (Item item in wornItems)
            {
                if (IsWearableTop(item.info))
                {
                    wearingTop = true;
                }
                if (IsWearableBottom(item.info))
                {
                    wearingBottom = true;
                }
            }

            // Check if top should be forced, and if so, give an appropriate item
            if (_config.ForceTopItem && !wearingTop)
            {
#if DEBUG
                LogWarning($"{player.displayName} is not wearing a top item; giving them one");
#endif
                if (availableTops.Count > 0)
                {
                    MoveAttire(player, availableTops);
                }
                else if (_topItemDefs.Count > 0)
                {
                    GiveAttire(player, _topItemDefs);
                }
            }

            // Check if bottom should be forced, and if so, give an appropriate item
            if (_config.ForceBottomItem && !wearingBottom)
            {
#if DEBUG
                LogWarning($"{player.displayName} is not wearing a bottom item; giving them one");
#endif
                if (availableBottoms.Count > 0)
                {
                    MoveAttire(player, availableBottoms);
                }
                else if (_bottomItemDefs.Count > 0)
                {
                    GiveAttire(player, _bottomItemDefs);
                }
            }

            Pool.FreeList(ref availableItems);
            Pool.FreeList(ref availableTops);
            Pool.FreeList(ref availableBottoms);
        }

        private void GiveAttire(BasePlayer player, Dictionary<ItemDefinition, ulong> itemDefinitons)
        {
            // Give player an item using a randomly selected skin from configuration
            KeyValuePair<ItemDefinition, ulong> itemDef = itemDefinitons.ElementAt(_random.Next(itemDefinitons.Count));
            Item item = ItemManager.Create(itemDef.Key, 1, itemDef.Value);
            if (item != null && !item.MoveToContainer(player.inventory.containerWear))
            {
                ItemManager.DoRemoves();
                player.inventory.ServerUpdate(0f);
            }
        }

        private void MoveAttire(BasePlayer player, List<Item> items)
        {
            // Force player to wear existing top item, if enabled
            if (_config.ForceTopItem)
            {
                Item topItem = null;
                foreach (Item item in items)
                {
                    if (IsWearableTop(item.info))
                    {
                        topItem = item;
                        break;
                    }
                }
                if (topItem != null && !topItem.MoveToContainer(player.inventory.containerWear))
                {
                    ItemManager.DoRemoves();
                    player.inventory.ServerUpdate(0f);
                }
            }

            // Force player to wear existing bottom item, if enabled
            if (_config.ForceBottomItem)
            {
                Item bottomItem = null;
                foreach (Item item in items)
                {
                    if (IsWearableBottom(item.info))
                    {
                        bottomItem = item;
                        break;
                    }
                }

                if (bottomItem != null && !bottomItem.MoveToContainer(player.inventory.containerWear))
                {
                    ItemManager.DoRemoves();
                    player.inventory.ServerUpdate(0f);
                }
            }
        }

        #endregion Item Handling

        #region Item Enforcement

        private object CanMoveItem(Item item, PlayerInventory inventory, uint targetContainer, int targetPosition)
        {
            BasePlayer player = inventory.baseEntity;
            if (player == null)
            {
                return null;
            }

            // TODO: Fix player becoming naked if full suit is worn

            bool isWearableTop = IsWearableTop(item.info);
            bool isWearableBottom = IsWearableBottom(item.info);

            if (targetContainer == inventory.containerWear.uid)
            {
                if (item.parent.uid == inventory.containerWear.uid)
                {
                    return null;
                }

                // Prevent replacing item in a taken slot
                if (player.inventory.containerWear.SlotTaken(item, targetPosition) && !isWearableTop && !isWearableBottom)
                {
                    return true; // Block moving
                }

                foreach (Item existingItem in inventory.containerWear.itemList)
                {
                    // Prevent the move unless the item and skin are different
                    if (existingItem.info.Equals(item.info) && existingItem.skin.Equals(item.skin))
                    {
#if DEBUG
                        LogWarning($"{player.displayName} tried to move {item.info.displayName.english} to {targetContainer}:{targetPosition}");
#endif
                        return true;
                    }
                }

                Item topItem;
                if (isWearableTop && IsWearingTop(player, out topItem) && topItem != null)
                {
#if DEBUG
                    LogWarning($"{player.displayName} tried to replace {topItem.info.displayName.english} with {item.info.displayName.english}");
#endif
                    if (!item.MoveToContainer(player.inventory.containerWear, topItem.position))
                    {
                        ItemManager.DoRemoves();
                        player.inventory.ServerUpdate(0f);
                    }
                    return true; // Block moving
                }

                Item bottomItem;
                if (isWearableBottom && IsWearingBottom(player, out bottomItem) && bottomItem != null)
                {
#if DEBUG
                    LogWarning($"{player.displayName} tried to replace {bottomItem.info.displayName.english} with {item.info.displayName.english}");
#endif
                    if (!item.MoveToContainer(player.inventory.containerWear, bottomItem.position))
                    {
                        ItemManager.DoRemoves();
                        player.inventory.ServerUpdate(0f);
                    }
                    return true; // Block moving
                }
            }

            // Prevent moving top/bottom items from wearable container
            if (item.parent.uid == inventory.containerWear.uid && (isWearableTop && _config.ForceTopItem || isWearableBottom && _config.ForceBottomItem))
            {
#if DEBUG
                LogWarning($"{player.displayName} tried to move top/bottom item from wearable container");
#endif
                return true; // Block moving
            }

            return null;
        }

        private object OnItemAction(Item item, string action)
        {
            // Only listen for drop attempts of worn items
            if (item.parent?.playerOwner == null || action != "drop" || item.parent.uid != item.parent.playerOwner.inventory.containerWear.uid)
            {
                return null;
            }

            // Check if top/bottom should be forced to be worn
            if ((_config.ForceTopItem && IsWearableTop(item.info)) || (_config.ForceBottomItem && IsWearableBottom(item.info)))
            {
#if DEBUG
                LogWarning("Blocked in drop check");
#endif
                return true; // Block dropping
            }

            return null;
        }

        #endregion Item Enforcement

        #region Helpers

        private bool IsWearableTop(ItemDefinition itemDef)
        {
            Wearable wearable = itemDef.ItemModWearable?.targetWearable;
            if (wearable != null)
            {
                bool isWearableTop = (wearable.occupationUnder & Wearable.OccupationSlots.TorsoFront) != 0;
#if DEBUG
                LogWarning($"================================================================");
                LogWarning($"Wearable slots for {itemDef.displayName.english}: {wearable.occupationUnder}");
                LogWarning($"Is {itemDef.displayName.english} a wearable top? {isWearableTop}");
#endif
                return isWearableTop;
            }

            return false;
        }

        private bool IsWearableBottom(ItemDefinition itemDef)
        {
            Wearable wearable = itemDef.ItemModWearable?.targetWearable;
            if (wearable != null)
            {
                bool isWearableBottom = (wearable.occupationUnder & (Wearable.OccupationSlots.Bum | Wearable.OccupationSlots.Groin)) != 0;
#if DEBUG
                LogWarning($"================================================================");
                LogWarning($"Wearable slots for {itemDef.displayName.english}: {wearable.occupationUnder}");
                LogWarning($"Is {itemDef.displayName.english} a wearable bottom? {isWearableBottom}");
#endif
                return isWearableBottom;
            }

            return false;
        }

        private bool IsWearingTop(BasePlayer player, out Item wearableTop)
        {
            foreach (Item item in player.inventory.containerWear.itemList)
            {
                if (IsWearableTop(item.info))
                {
                    wearableTop = item;
                    return true;
                }
            }

            wearableTop = null;
            return false;
        }

        private bool IsWearingBottom(BasePlayer player, out Item wearableBottom)
        {
            foreach (Item item in player.inventory.containerWear.itemList)
            {
                if (IsWearableBottom(item.info))
                {
                    wearableBottom = item;
                    return true;
                }
            }

            wearableBottom = null;
            return false;
        }

        #endregion Helpers
    }
}
