﻿using Oxide.Core.Libraries.Covalence;
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEngine;
using Oxide.Core;
using System;
using Facepunch;

namespace Oxide.Plugins
{
    [Info("Recycle", "nivex", "3.2.1")]
    [Description("Recycle items into their resources")]
    public class Recycle : RustPlugin
    {
        private const string 
            RecyclePrefab = "assets/bundled/prefabs/static/recycler_static.prefab",
            BackpackPrefab = "assets/prefabs/misc/item drop/item_drop_backpack.prefab",
            AdminPermission = "recycle.admin",
            RecyclerPermission = "recycle.use",
            CooldownBypassPermission = "recycle.bypass";

        private readonly Dictionary<ulong, DroppedInfo> _droppedContainers = new();
        private readonly Dictionary<ulong, RecyclerInfo> _recyclers = new();
        private readonly Dictionary<string, long> _cooldowns = new();
        private ConfigData config;

        public class DroppedInfo
        {
            public DroppedItemContainer container;
            public BasePlayer target;
            public DroppedInfo(DroppedItemContainer container, BasePlayer target)
            {
                this.container = container;
                this.target = target;
            }
        }

        public class RecyclerInfo
        {
            public Recycler recycler;
            public BasePlayer player;
            public ItemContainerId id;
            public RecyclerInfo(Recycler recycler, BasePlayer player)
            {
                this.recycler = recycler;
                this.player = player;
                id = recycler.inventory.uid;
            }
        }

        #region Hooks

        private void Loaded()
        {
            string recycleCommand = string.IsNullOrEmpty(config.Settings.RecycleCommand) ? "recycle" : config.Settings.RecycleCommand;
            AddCovalenceCommand(recycleCommand, "RecycleCommand");
            AddCovalenceCommand("purgerecyclers", "PurgeRecyclersCommand");
            AddCovalenceCommand("purgebags", "PurgeBagsCommand");
            permission.RegisterPermission(AdminPermission, this);
            permission.RegisterPermission(RecyclerPermission, this);
            permission.RegisterPermission(CooldownBypassPermission, this);
            if (!config.Settings.ToInventory) Unsubscribe(nameof(CanMoveItem));
        }

        private void Unload()
        {
            DestroyRecyclers();
            DestroyBags();
        }

        private void OnLootEntityEnd(BasePlayer player, Recycler recycler)
        {
            if (player != null) DestroyRecycler(player);
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player == null) return;

            DestroyRecycler(player);

            if (_droppedContainers.Count == 0) return;

            using var tmp = Pool.Get<PooledList<KeyValuePair<ulong, DroppedInfo>>>();
            tmp.AddRange(_droppedContainers);

            foreach (var (uid, val) in tmp)
            {
                if (!IsValid(val.container) || !IsValid(val.target) || val.target.userID == player.userID)
                {
                    if (IsValid(val.container))
                        val.container.Kill();

                    _droppedContainers.Remove(uid);
                }
            }
        }

        private object CanMoveItem(Item item, PlayerInventory inv, ItemContainerId targetContainerId, int targetSlot, int amount)
        {
            if (targetSlot < 6) return null;

            foreach (ItemContainer container in inv.loot.containers)
            {
                if (container.uid != targetContainerId || container.entityOwner == null) continue;

                if (container.entityOwner is Recycler recycler && IsRecycleBox(recycler)) return false;
            }

            return null;
        }

        private float lastMessageTime;
        private object CanAcceptItem(ItemContainer container, Item item, int targetPos)
        {
            if (container == null || !(container.entityOwner is Recycler recycler)) return null;

            if (!IsRecycleBox(recycler)) return null;

            BasePlayer player = PlayerFromRecycler(recycler.net.ID.Value);

            if (player == null) return null;
            
            string type = Enum.GetName(typeof(ItemCategory), item.info.category);

            if (targetPos < 6)
            {
                if (!config.Settings.RecyclableTypes.Contains(type) || config.Settings.Blacklist.Contains(item.info.shortname))
                {
                    if (lastMessageTime < Time.time)
                    {
                        lastMessageTime = Time.time + 0.1f;
                        Message(player, "Recycle", "Invalid");
                    }
                    return ItemContainer.CanAcceptResult.CannotAcceptRightNow;
                }

                recycler.Invoke(() =>
                {
                    if (recycler.IsOn()) return;

                    if (!recycler.HasRecyclable()) return;

                    float time = config.Settings.InstantRecycling ? 0.0625f : recycler.GetRecycleThinkDuration();

                    recycler.InvokeRepeating(recycler.RecycleThink, time, time);
                    recycler.SetFlag(BaseEntity.Flags.On, b: true);
                    recycler.SendNetworkUpdateImmediate();
                }, 0.0625f);
            }
            else if (config.Settings.ToInventory && item.GetOwnerPlayer() == null)
            {
                Item copy = ItemManager.Create(item.info, item.amount, item.skin);
                if (player.inventory.GiveItem(copy)) item.Remove();
            }

            return null;
        }

        private object CanLootEntity(BasePlayer player, DroppedItemContainer container)
        {
            if (container.IsValid() && _droppedContainers.TryGetValue(container.net.ID.Value, out var t) && t.target.userID != player.userID) return true;
            return null;
        }

        private void OnEntityKill(DroppedItemContainer container)
        {
            if (container.IsValid()) _droppedContainers.Remove(container.net.ID.Value);
        }

        private void OnUseNPC(BasePlayer npc, BasePlayer player) // HumanNPC plugin support
        {
            if (npc == null || !config.Settings.NPCIds.Contains(npc.UserIDString)) return;
            OpenRecycler(player);
        }

        #endregion

        #region Commands

        private void RecycleCommand(IPlayer user, string command, string[] args)
        {
            if (CanManageRecyclers(user) && args.Contains("reloadconfig"))
            {
                LoadConfig();
                Message(user, "Recycle", "Reloaded");
            }

            if (config.Settings.NPCOnly)
                return;

            BasePlayer player = user.Object as BasePlayer;
            
            if (player == null || !CanPlayerOpenRecycler(player)) 
                return;

            OpenRecycler(player);

            if (config.Settings.Cooldown > 0 && !CanBypassCooldown(user)) 
                _cooldowns[player.UserIDString] = DateTimeOffset.Now.ToUnixTimeSeconds() + (long)(config.Settings.Cooldown * 60);
        }

        private void PurgeRecyclersCommand(IPlayer user, string command, string[] args)
        {
            if (CanManageRecyclers(user))
            {
                DestroyRecyclers();
                Message(user, "Recycle", "DestroyedAll");
            }
            else Message(user, "Denied", "Permission");
        }

        private void PurgeBagsCommand(IPlayer user, string command, string[] args)
        {
            if (CanManageRecyclers(user))
            {
                DestroyBags();
                Message(user, "Recycle", "DestroyedAllBags");
            }
            else Message(user, "Denied", "Permission");
        }

        private void Message(IPlayer user, string top, string bottom)
        {
            if (user.Object is BasePlayer player)
            {
                Message(player, top, bottom);
                return;
            }

            string message = GetMessage(top, bottom, user.Id);

            if (string.IsNullOrEmpty(message))
            {
                return; // set a message value to empty to disable that message
            }

            if (user.IsServer)
            {
                Puts(message);
            }
            else
            {
                user.Message(message);
            }
        }

        private void Message(BasePlayer player, string top, string bottom, params object[] args)
        {
            if (player == null) return;
            string message = GetMessage(top, bottom, player.UserIDString);
            if (string.IsNullOrEmpty(message)) return;
            PrintToChat(player, args.Length > 0 ? string.Format(message, args) : message);
        }

        public bool IsValid(BaseNetworkable e) => e.IsValid() && !e.IsDestroyed;

        #endregion

        #region Helpers

        private Recycler CreateRecycler(BasePlayer player)
        {
            var recycler = GameManager.server.CreateEntity(RecyclePrefab, player.transform.position.WithY(-5f)) as Recycler;

            if (recycler == null) return null;

            recycler.enableSaving = false;
            recycler.Spawn();

            if (!IsValid(recycler)) return null;

            recycler.radtownRecycleEfficiency = config.Settings.RefundRatio;
            recycler.safezoneRecycleEfficiency = config.Settings.RefundRatio;
            recycler.SetFlag(BaseEntity.Flags.Locked, true);
            recycler.UpdateNetworkGroup();
            recycler.gameObject.layer = 0;
            recycler.SendNetworkUpdateImmediate();

            OpenContainer(player, recycler);

            _recyclers.Add(recycler.net.ID.Value, new(recycler, player));

            return recycler;
        }

        private void OpenContainer(BasePlayer player, StorageContainer container)
        {
            player.Invoke(() =>
            {
                if (container == null || container.IsDestroyed || player.IsDestroyed) return;
                player.EndLooting();
                if (!player.inventory.loot.StartLootingEntity(container, false)) return;
                player.inventory.loot.AddContainer(container.inventory);
                player.inventory.loot.SendImmediate();
                player.ClientRPC(RpcTarget.Player("RPC_OpenLootPanel", player), container.panelName);
                player.SendNetworkUpdate();
            }, 0.2f);
        }

        private void DropRecyclerContents(Recycler recycler, BasePlayer player)
        {
            if (player == null || player.inventory == null || player.inventory.containerMain == null || player.inventory.containerBelt == null) return;
            if (recycler == null || recycler.inventory == null || recycler.inventory.itemList.IsNullOrEmpty()) return;

            using var items = Pool.Get<PooledList<Item>>();

            items.AddRange(recycler.inventory.itemList);

            if (config.Settings.InventoryBeforeBag)
            {
                for (int i = 0; i < items.Count; i++)
                {
                    Item item = items[i];

                    if (player.inventory.GiveItem(item))
                    {
                        items.RemoveAt(i);
                        i--;
                    }
                }
            }

            if (items.Count == 0)
            {
                return;
            }

            Message(player, "Recycle", "Dropped");

            var container = GameManager.server.CreateEntity(BackpackPrefab, player.transform.position + Vector3.up) as DroppedItemContainer;

            if (container == null) return;

            container.enableSaving = false;
            container.lootPanelName = "generic_resizable";
            container.playerSteamID = player.userID;
            container.TakeFrom(new[] { recycler.inventory }, 0f);
            container.Spawn();

            if (IsValid(container))
            {
                _droppedContainers[container.net.ID.Value] = new(container, player);
            }
        }

        private void DestroyRecycler(BasePlayer player)
        {
            Recycler recycler = RecyclerFromPlayer(player.userID);

            if (IsValid(recycler) && _recyclers.TryGetValue(recycler.net.ID.Value, out var t))
            {
                DropRecyclerContents(recycler, t.player);
                _recyclers.Remove(recycler.net.ID.Value);
                recycler.Kill();
            }
        }

        private void DestroyRecyclers()
        {
            if (_recyclers.Count == 0) return;
            using var tmp = Pool.Get<PooledList<RecyclerInfo>>();
            tmp.AddRange(_recyclers.Values);
            foreach (var val in tmp)
            {
                if (IsValid(val.recycler))
                {
                    DropRecyclerContents(val.recycler, val.player);
                    val.recycler.Kill();
                }
            }
            _recyclers.Clear();
        }

        private void DestroyBags()
        {
            if (_droppedContainers.Count == 0) return;
            using var tmp = Pool.Get<PooledList<DroppedInfo>>();
            tmp.AddRange(_droppedContainers.Values);
            foreach (var val in tmp)
            {
                if (IsValid(val.container))
                {
                    val.container.Kill();
                }
            }
            _droppedContainers.Clear();
        }

        private string GetMessage(string top, string bottom, string userid)
        {
            return lang.GetMessage(top + " -> " + bottom, this, userid);
        }

        private int[] GetCooldown(string userid)
        {
            if (!_cooldowns.TryGetValue(userid, out var time)) return Array.Empty<int>();

            long now = DateTimeOffset.Now.ToUnixTimeSeconds();

            if (now > time) return Array.Empty<int>();

            TimeSpan diff = TimeSpan.FromSeconds(time - DateTimeOffset.Now.ToUnixTimeSeconds());

            return new int[] { diff.Minutes, diff.Seconds };
        }

        private string CooldownTimesToString(int[] times, BasePlayer player)
        {
            if (times == null || times.Length != 2) return string.Empty;

            int mins = times[0], secs = times[1];

            return (string.Format(
                mins == 0 ? string.Empty : ("{0} " + GetMessage("Timings", mins == 1 ? "minute" : "minutes", player.UserIDString)), mins) +
                string.Format(" {0} " + GetMessage("Timings", secs == 1 ? "second" : "seconds", player.UserIDString), secs)
            ).Trim();
        }

        #endregion

        #region API

        private BasePlayer PlayerFromRecycler(ulong netID) => _recyclers.TryGetValue(netID, out var t) ? t.player : null;

        private Recycler RecyclerFromPlayer(ulong userid)
        {
            foreach (var val in _recyclers.Values)
                if (val.player?.userID == userid)
                    return val.recycler;
            return null;
        }

        private bool IsOnCooldown(IPlayer user) => config.Settings.Cooldown > 0 && !CanBypassCooldown(user) && _cooldowns.ContainsKey(user.Id) && DateTimeOffset.Now.ToUnixTimeSeconds() < _cooldowns[user.Id];

        private bool CanUseRecycler(IPlayer user) => user.HasPermission(RecyclerPermission);

        private bool CanManageRecyclers(IPlayer user) => user.HasPermission(AdminPermission);

        private bool CanBypassCooldown(IPlayer user) => user.HasPermission(CooldownBypassPermission);

        private bool IsRecycleBox(BaseNetworkable e) => IsValid(e) && _recyclers.ContainsKey(e.net.ID.Value);

        private bool CanPlayerOpenRecycler(BasePlayer player)
        {
            if (player == null || !(player.IPlayer is IPlayer user) || !player.IsAlive())
                Message(player, "Denied", "Hook Denied");
            else if (!CanUseRecycler(user) && !CanManageRecyclers(user))
                Message(player, "Denied", "Permission");
            else if (IsOnCooldown(user))
                Message(player, "Cooldown", "In", CooldownTimesToString(GetCooldown(player.UserIDString), player));
            else if (player.IsWounded())
                Message(player, "Denied", "Wounded");
            else if (!player.CanBuild())
                Message(player, "Denied", "Privilege");
            else if (config.Settings.RadiationMax > 0 && player.radiationLevel > config.Settings.RadiationMax)
                Message(player, "Denied", "Irradiation");
            else if (player.IsSwimming())
                Message(player, "Denied", "Swimming");
            else if (!player.IsOnGround() || player.IsFlying || player.isInAir)
                Message(player, "Denied", "Falling");
            else if (player.isMounted || player.GetParentEntity() is BaseMountable)
                Message(player, "Denied", "Mounted");
            else if (player.GetComponentInParent<CargoShip>())
                Message(player, "Denied", "Ship");
            else if (player.GetComponentInParent<HotAirBalloon>())
                Message(player, "Denied", "Balloon");
            else if (player.GetComponentInParent<Lift>())
                Message(player, "Denied", "Elevator");
            else if (!config.Settings.AllowedInSafeZones && player.InSafeZone())
                Message(player, "Denied", "Safe Zone");
            else if (Interface.Call("CanOpenRecycler", player) is object obj && obj != null && (obj is not bool val || !val))
                Message(player, "Denied", obj is string str && str.Length > 0 ? str : "Hook Denied");
            else
                return true;

            return false;
        }

        private void OpenRecycler(BasePlayer player)
        {
            if (player == null) 
                return;

            DestroyRecycler(player);
            CreateRecycler(player);
        }

        private void AddNpc(string id)
        {
            if (config.Settings.NPCIds.Contains(id)) 
                return;

            config.Settings.NPCIds.Add(id);
            SaveConfig();
        }

        private void RemoveNpc(string id)
        {
            if (config.Settings.NPCIds.Remove(id)) 
                SaveConfig();
        }

        #endregion


        #region Structs

        public class ConfigData
        {
            public class SettingsWrapper
            {
                [JsonProperty("Command To Open Recycler")]
                public string RecycleCommand = "recycle";

                [JsonProperty("Cooldown (in minutes)")]
                public float Cooldown = 5.0f;

                [JsonProperty("Maximum Radiation")]
                public float RadiationMax = 1f;

                [JsonProperty("Refund Ratio")]
                public float RefundRatio = 0.5f;

                [JsonProperty("NPCs Only")]
                public bool NPCOnly;

                [JsonProperty("Allowed In Safe Zones")]
                public bool AllowedInSafeZones = true;

                [JsonProperty("Instant Recycling")]
                public bool InstantRecycling = false;

                [JsonProperty("Send Recycled Items To Inventory")]
                public bool ToInventory = true;

                [JsonProperty("Send Items To Inventory Before Bag")]
                public bool InventoryBeforeBag = true;

                [JsonProperty("NPC Ids", ObjectCreationHandling = ObjectCreationHandling.Replace)]
                public List<object> NPCIds = new();

                [JsonProperty("Recyclable Types", ObjectCreationHandling = ObjectCreationHandling.Replace)]
                public List<object> RecyclableTypes = new();

                [JsonProperty("Blacklisted Items", ObjectCreationHandling = ObjectCreationHandling.Replace)]
                public List<object> Blacklist = new();
            }

            public SettingsWrapper Settings = new();
            public string VERSION = "3.2.0";
        }

        #endregion

        #region Configuration

        protected override void LoadDefaultMessages()
        {
            Func<string, string> youCannot = (thing) => "You cannot recycle while " + thing;

            lang.RegisterMessages(new Dictionary<string, string>
            {
                { "Recycle -> Reloaded", "Configuration file has been reloaded" },
                { "Recycle -> DestroyedAllBags", "All bags have been destroyed" },
                { "Recycle -> DestroyedAll", "All recyclers have been destroyed" },
                { "Recycle -> Dropped", "You left some items in the recycler!" },
                { "Recycle -> Invalid", "You cannot recycle that!" },
                { "Denied -> Npc Only", "You must use the recycler at specific npcs only" },
                { "Denied -> Permission", "You don't have permission to use that command" },
                { "Denied -> Privilege", "You cannot recycle within someone's building privilege" },
                { "Denied -> Swimming", youCannot("swimming") },
                { "Denied -> Falling", youCannot("falling") },
                { "Denied -> Mounted", youCannot("mounted") },
                { "Denied -> Wounded", youCannot("wounded") },
                { "Denied -> Irradiation", youCannot("irradiated") },
                { "Denied -> Ship", youCannot("on a ship") },
                { "Denied -> Elevator", youCannot("on an elevator") },
                { "Denied -> Balloon", youCannot("on a balloon") },
                { "Denied -> Safe Zone", youCannot("in a safe zone") },
                { "Denied -> Hook Denied", "You can't recycle right now" },
                { "Cooldown -> In", "You need to wait {0} before recycling" },
                { "Timings -> second", "second" },
                { "Timings -> seconds", "seconds" },
                { "Timings -> minute", "minute" },
                { "Timings -> minutes", "minutes" }
            }, this);
        }

        protected override void LoadDefaultConfig()
        {
            config = new()
            {
                Settings =
                {
                    RecyclableTypes = new()
                    {
                        "Ammunition", "Attire", "Component", "Construction", "Electrical", "Fun", "Items", "Medical", "Misc", "Resources", "Tool", "Traps", "Weapon"
                    }
                }
            };
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            canSaveConfig = false;
            try
            {
                config = Config.ReadObject<ConfigData>();
                config ??= new();
                config.Settings ??= new();
                config.Settings.NPCIds ??= new();
                canSaveConfig = true;
                SaveConfig();
            }
            catch (Exception ex)
            {
                Puts(ex.ToString());
                LoadDefaultConfig();
            }
        }

        private bool canSaveConfig = true;

        protected override void SaveConfig()
        {
            if (canSaveConfig)
            {
                config.VERSION = Version.ToString();
                Config.WriteObject(config, true);
            }
        }

        #endregion
    }
}