using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;

using Oxide.Core;
using Oxide.Core.Plugins;

using UnityEngine;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Oxide.Plugins
{
    [Info ("Bank", "Calytic", "1.0.56")]
    [Description ("Safe player storage")]
    class Bank : RustPlugin
    {
        #region Variables

        string defaultBoxPrefab;
        int defaultSlots;

        Dictionary<string, object> boxPrefabs;
        Dictionary<string, object> boxSlots;
        bool keyring;
        float cooldownMinutes;
        bool npconly;
        List<string> npcids = new List<string> ();
        float radiationMax;
        bool allowSafeZone;

        public static DataFileSystem datafile;
        public static DataFileSystem configFile;

        Dictionary<string, object> itemLimits = new Dictionary<string, object> ();

        [PluginReference]
        Plugin MasterKey;

        #endregion

        #region Bank/Item Profile

        public class ItemProfile
        {
            public string id;
            public int amount;
            public int slot;
            public int capacity;
            public Item.Flag flags;
            public float condition;
            public ulong skin;
            public List<ItemProfile> contents;
            public int primaryMagazine;
            public int ammoType;
            public int dataInt;

            [JsonConstructor]
            public ItemProfile (string id, int amount, int slot, int capacity, Item.Flag flags, float condition = 0.0f, ulong skin = 0, List<ItemProfile> contents = null, int primaryMagazine = 0, int ammoType = 0, int dataInt = 0)
            {
                this.id = id;
                this.amount = amount;
                this.slot = slot;
                this.capacity = capacity;
                this.flags = flags;
                this.condition = condition;
                this.skin = skin;
                this.contents = contents;
                this.primaryMagazine = primaryMagazine;
                this.ammoType = ammoType;
                this.dataInt = dataInt;
            }

            public static ItemProfile Create (Item item)
            {
                List<ItemProfile> contents = new List<ItemProfile> ();
                int primaryMagazine = 0;
                int ammoType = 0;

                if (item.contents != null) {
                    if (item.contents.itemList.Count > 0) {
                        foreach (Item content in item.contents.itemList) {
                            contents.Add (Create (content));
                        }
                    }
                }

                var weapon = item.GetHeldEntity () as BaseProjectile;
                if (weapon != null) {
                    BaseProjectile projectile = weapon as BaseProjectile;
                    primaryMagazine = projectile.primaryMagazine.contents;
                    ammoType = projectile.primaryMagazine.ammoType.itemid;
                }


                int dataInt = 0;
                if (item.instanceData != null && item.info.shortname == "door.key") {
                    dataInt = item.instanceData.dataInt;
                }

                ItemProfile newItem = new ItemProfile (item.info.itemid.ToString (), item.amount, item.position, item.contents?.capacity ?? -1, item.flags, item.condition, item.skin, contents, primaryMagazine, ammoType, dataInt);

                return newItem;
            }
        }

        public class BankProfile
        {
            protected ulong playerID;

            public List<ItemProfile> items = new List<ItemProfile> ();

            [JsonIgnore]
            public bool open = false;

            [JsonIgnore]
            public bool dirty = false;

            [JsonIgnore]
            public BasePlayer Player {
                get {
                    return BasePlayer.Find (playerID.ToString ());
                }
                protected set {
                    playerID = value.userID;
                }
            }

            [JsonIgnore]
            public ulong PlayerID {
                get { return playerID; }
                private set { }
            }

            [JsonIgnore]
            public int Count {
                get {
                    return items.Count;
                }
                private set { }
            }

            public BankProfile ()
            {

            }

            public BankProfile (BasePlayer player, List<ItemProfile> items = null)
            {
                playerID = player.userID;
                if (items is List<ItemProfile>) {
                    this.items = items;
                }
            }

            [JsonConstructor]
            public BankProfile (ulong playerID, List<ItemProfile> items)
            {
                this.playerID = playerID;
                this.items = items;
            }

            public bool Add (Item item)
            {
                ItemProfile profile = ItemProfile.Create (item);
                items.Add (profile);
                dirty = true;

                return true;
            }

            public bool Add (Item [] items)
            {
                foreach (Item item in items) {
                    if (!Add (item)) {
                        return false;
                    }
                }

                return true;
            }

            public bool Add (List<Item> items)
            {
                return Add (items.ToArray ());
            }

            public bool Remove (Item item)
            {
                ItemProfile removing = null;
                foreach (ItemProfile profile in this.items) {
                    if (profile.id == item.info.itemid.ToString () && profile.amount == item.amount) {
                        removing = profile;
                        break;
                    }
                }

                if (removing is ItemProfile) {
                    dirty = true;
                    items.Remove (removing);
                    return true;
                }

                return false;
            }

            public bool Remove (Item [] items)
            {
                foreach (Item item in items) {
                    if (!Remove (item)) {
                        return false;
                    }
                }

                return true;
            }

            public bool Remove (List<Item> items)
            {
                return Remove (items.ToArray ());
            }

            [JsonIgnore]
            ItemContainer container;

            public ItemContainer SetContainer(ItemContainer container, BasePlayer player, int slots = 30)
            {

                if (this.container is ItemContainer) {
                    this.container.playerOwner = player;
                    this.container.itemList.Clear ();
                    PopulateContainer (player, this.container);
                    return this.container;
                }
                container.playerOwner = player;
                PopulateContainer(player, container);                

                return this.container = container;
            }

            void PopulateContainer (BasePlayer player, ItemContainer container, List<ItemProfile> items = null)
            {
                if (items == null) {
                    items = this.items;
                }

                
                foreach (ItemProfile profile in items) {
                    Item item = ItemManager.CreateByItemID (Convert.ToInt32 (profile.id), profile.amount);
                    var numberOfSlots = Math.Max(profile.capacity, profile.contents?.Count ?? -1);
                    if (item.contents == null && numberOfSlots > 0)
                    {
                        foreach (var itemmod in item.info.itemMods)
                        {
                            if (itemmod is ItemModContainerArmorSlot imcas)
                            {
                                imcas.CreateAtCapacity(numberOfSlots, item);
                            }
                        }
                    }

                    if (item is Item) {
                        item.flags = profile.flags;
                        item.skin = profile.skin;

                        if (item.hasCondition) {
                            item.condition = profile.condition;
                        }

                        var held = item.GetHeldEntity ();
                        if (held is BaseEntity) {
                            held.skinID = profile.skin;
                        }
                        var weapon = held as BaseProjectile;
                        if (weapon != null) {
                            BaseProjectile projectile = weapon as BaseProjectile;
                            projectile.primaryMagazine.contents = profile.primaryMagazine;
                            if (profile.ammoType != 0) {
                                projectile.primaryMagazine.ammoType = ItemManager.FindItemDefinition (profile.ammoType);
                            }
                        }

                        if (profile.contents != null) {
                            if (profile.contents.Count > 0) {
                                PopulateContainer (player, item.contents, profile.contents);
                            }
                        }

                        if (item.info.shortname == "door.key" && profile.dataInt != 0) {
                            ProtoBuf.Item.InstanceData instanceData = Facepunch.Pool.Get<ProtoBuf.Item.InstanceData> ();
                            item.instanceData = instanceData;
                            item.instanceData.ShouldPool = false;
                            item.instanceData.dataInt = profile.dataInt;
                        }

                        if (container!=null) item.MoveToContainer (container, profile.slot);
                    }
                }
            }
        }
        #endregion

        #region Configuration

        class ItemLimit
        {
            public bool enabled;
            public int minimum;
            public int maximum;

            [JsonConstructor]
            public ItemLimit (bool enabled, int minimum, int maximum)
            {
                this.enabled = enabled;
                this.minimum = minimum;
                this.maximum = maximum;
            }
        }

        #endregion

        #region State

        class OnlinePlayer
        {
            public BasePlayer Player;
            public BasePlayer Target;
            public StorageContainer View;
            public List<BasePlayer> Matches;

            public OnlinePlayer (BasePlayer player)
            {
            }
        }

        public Dictionary<ItemContainer, ulong> containers = new Dictionary<ItemContainer, ulong> ();
        public Dictionary<ulong, BankProfile> banks = new Dictionary<ulong, BankProfile> ();

        [OnlinePlayers]
        Hash<BasePlayer, OnlinePlayer> onlinePlayers = new Hash<BasePlayer, OnlinePlayer> ();
        Dictionary<string, DateTime> bankCooldowns = new Dictionary<string, DateTime> ();

        #endregion

        #region Initialization & Data

        void Init ()
        {
            Unsubscribe (nameof (OnEntityTakeDamage));

            configFile = new DataFileSystem (Interface.Oxide.ConfigDirectory);
            itemLimits = configFile.ReadObject<Dictionary<string, object>> ("Bank_ItemLimits");
            permission.RegisterPermission ("bank.use", this);

            CheckConfig ();

            datafile = new DataFileSystem (Interface.Oxide.DataDirectory + "\\" + this.GetConfig<string> ("subDirectory", "banks"));

            boxPrefabs = GetConfig ("Settings", "boxes", GetDefaultBoxes ());
            boxSlots = GetConfig ("Settings", "slots", GetDefaultSlots ());

            defaultBoxPrefab = GetConfig ("Settings", "defaultBox", "assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab");
            defaultSlots = GetConfig ("Settings", "defaultSlots", 4);

            cooldownMinutes = GetConfig ("Settings", "cooldownMinutes", 5f);
            radiationMax = GetConfig ("Settings", "radiationMax", 1f);
            allowSafeZone = GetConfig ("Settings", "allowSafeZone", true);

            npconly = GetConfig ("Settings", "NPCBankersOnly", false);
            List<object> lnpcids = GetConfig ("Settings", "NPCIDs", new List<object> ());

            foreach (object id in lnpcids) {
                npcids.Add (id.ToString ());
            }

            keyring = GetConfig ("Settings", "Keyring", true);

            foreach (KeyValuePair<string, object> kvp in boxPrefabs) {
                permission.RegisterPermission (kvp.Key, this);
            }

            foreach (KeyValuePair<string, object> kvp in boxSlots) {
                if (!boxPrefabs.ContainsKey (kvp.Key)) {
                    permission.RegisterPermission (kvp.Key, this);
                }
            }

            foreach (BasePlayer player in BasePlayer.activePlayerList) {
                LoadProfile (player.userID);
            }
        }

        new void LoadDefaultMessages ()
        {
            lang.RegisterMessages (new Dictionary<string, string>
            {
                {"Denied: Permission", "You lack permission to do that"},
                {"Denied: Privilege", "You lack build privileges and cannot do that"},
                {"Denied: Swimming", "You cannot do that while swimming"},
                {"Denied: Falling", "You cannot do that while falling"},
                {"Denied: Wounded", "You cannot do that while wounded"},
                {"Denied: Mounted", "You cannot do that while mounted"},
                {"Denied: Generic", "You cannot do that right now"},
                {"Denied: Irradiated", "You cannot do that while irradiated"},
                {"Denied: Ship", "You cannot do that while on a ship"},
                {"Denied: Lift", "You cannot do that while on a lift"},
                {"Denied: Balloon", "You cannot do that while on a balloon"},
                {"Denied: Safe Zone", "You cannot do that while in a safe zone"},
                {"Cooldown: Seconds", "You are doing that too often, try again in a {0} seconds(s)."},
                {"Cooldown: Minutes", "You are doing that too often, try again in a {0} minute(s)."},
                {"Limit: Return", "Some items were returned to your inventory"}
            }, this);
        }

        Dictionary<string, object> GetDefaultBoxes ()
        {
            return new Dictionary<string, object> () {
                {"bank.default", "assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab"},
                {"bank.big", "assets/prefabs/deployable/large wood storage/box.wooden.large.prefab"}
            };
        }

        Dictionary<string, object> GetDefaultSlots ()
        {
            return new Dictionary<string, object> () {
                {"bank.default", 4},
                {"bank.big", 30}
            };
        }

        Dictionary<string, object> GetDefaultItemLimits ()
        {
            return new Dictionary<string, object> () {
                {"explosive.timed", new ItemLimit(false, 0, 1)}
            };
        }

        protected override void LoadDefaultConfig ()
        {
            Config ["Settings", "boxes"] = GetDefaultBoxes ();
            Config ["Settings", "slots"] = GetDefaultSlots ();
            Config ["Settings", "defaultBox"] = "assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab";
            Config ["Settings", "defaultSlots"] = 4;
            Config ["Settings", "keyring"] = true;
            Config ["Settings", "cooldownMinutes"] = 5;
            Config ["Settings", "radiationMax"] = 1;
            Config ["Settings", "NPCBankersOnly"] = false;
            Config ["Settings", "NPCIDs"] = new List<object> ();
            Config ["Settings", "allowSafeZone"] = true;

            if (configFile == null)
                configFile = new DataFileSystem (Interface.Oxide.ConfigDirectory);

            configFile.WriteObject ("Bank_ItemLimits", GetDefaultItemLimits ());

            Config ["VERSION"] = Version.ToString ();
        }

        void Unload ()
        {
            foreach (var player in BasePlayer.activePlayerList) {
                OnlinePlayer onlinePlayer;
                if (onlinePlayers.TryGetValue (player, out onlinePlayer) && onlinePlayer.View != null) {
                    SaveProfileByUser (player.userID);
                    CloseBank (player, onlinePlayer.View);
                }
            }
        }

        void OnServerSave ()
        {
            SaveData ();
        }

        void CheckConfig ()
        {
            if (Config ["VERSION"] == null) {
                // FOR COMPATIBILITY WITH INITIAL VERSIONS WITHOUT VERSIONED CONFIG
                ReloadConfig ();
            } else if (GetConfig<string> ("VERSION", "") != Version.ToString ()) {
                // ADDS NEW, IF ANY, CONFIGURATION OPTIONS
                ReloadConfig ();
            }
        }

        protected void ReloadConfig ()
        {
            Config ["VERSION"] = Version.ToString ();

            // NEW CONFIGURATION OPTIONS HERE
            Config ["Settings", "radiationMax"] = GetConfig ("Settings", "radiationMax", 1f);
            Config ["Settings", "NPCBankersOnly"] = GetConfig ("Settings", "NPCBankersOnly", false);
            Config ["Settings", "NPCIDs"] = GetConfig ("Settings", "NPCIDs", new List<object> ());
            if (itemLimits.Count == 0) {
                itemLimits = GetDefaultItemLimits ();
            }
            configFile.WriteObject ("Bank_ItemLimits", itemLimits);
            // END NEW CONFIGURATION OPTIONS

            PrintWarning ("Upgrading configuration file");
            SaveConfig ();
        }

        void SaveProfileByUser (ulong userID)
        {
            if (banks.ContainsKey (userID)) {
                SaveProfile (userID, banks [userID]);
            }
        }

        void SaveData ()
        {
            int t = 0;

            foreach (KeyValuePair<ulong, BankProfile> kvp in banks) {
                if (kvp.Value.dirty) {
                    SaveProfile (kvp.Key, kvp.Value);
                    t++;
                }
            }

            PrintToConsole ("Saved " + t.ToString () + " banks");
        }

        protected bool LoadProfile (ulong playerID, bool reload = false)
        {
#if DEBUG
            Puts ($"Loading Bank {playerID}");
#endif

            if (playerID == 0) {
                return false;
            }

            //if (banks.ContainsKey (playerID)) {
            //    return true;
            //}

            string path = "bank_" + playerID.ToString ();

            BankProfile profile = datafile.ReadObject<BankProfile> (path);

            if (!(profile is BankProfile)) {
                return false;
            }

            if (profile.Count == 0) {
                return false;
            }

            if (banks.ContainsKey (playerID)) {
                banks [playerID] = profile;
            } else {
                banks.Add (playerID, profile);
            }

            return true;
        }

        void SaveProfile (ulong playerID, BankProfile profile = null)
        {
            if (profile == null) {
                if (!banks.ContainsKey (playerID)) {
                    return;
                }
                profile = banks [playerID];
            }

            string path = "bank_" + playerID.ToString ();
            int pc = profile.Count;
            datafile.WriteObject (path, profile);
            profile.dirty = false;
        }

        #endregion

        #region API

        bool IsBankBox (BaseNetworkable entity)
        {
            foreach (KeyValuePair<BasePlayer, OnlinePlayer> kvp in onlinePlayers) {
                if (kvp.Value?.View?.net != null && entity?.net != null && kvp.Value.View.net.ID == entity.net.ID) {
                    return true;
                }
            }

            return false;
        }

        bool TryGetPlayer(BaseNetworkable entity, out BasePlayer player)
        {
            player = null;
            foreach (KeyValuePair<BasePlayer, OnlinePlayer> kvp in onlinePlayers)
            {
                if (kvp.Value?.View?.net != null && entity?.net != null && kvp.Value.View.net.ID == entity.net.ID)
                {
                    player = kvp.Value.Player;
                    return true;
                }
            }

            return false;
        }

        void AddNpc (string id)
        {
            npcids.Add (id);
        }

        void AddNpc (ulong id)
        {
            AddNpc (id.ToString ());
        }

        void RemoveNpc (string id)
        {
            if (npcids.Contains (id)) {
                npcids.Remove (id);
            }
        }

        void RemoveNpc (ulong id)
        {
            RemoveNpc (id.ToString ());
        }

        #endregion

        #region Oxide Hooks

        void OnUseNPC (BasePlayer npc, BasePlayer player)
        {
            if (!npcids.Contains (npc.UserIDString)) return;
            ShowBank (player, player);
        }


        object OnEntityTakeDamage (BaseCombatEntity entity, HitInfo hitInfo)
        {
            if (hitInfo == null)
                return null;

            if (entity == null)
                return null;

            if (IsBankBox (entity))
                return false;

            return null;
        }


        object CanUseLockedEntity (BasePlayer player, BaseLock lockItem)
        {
            if (player == null) {
                return null;
            }

            if (lockItem == null) {
                return null;
            }

            if (!keyring) {
                return null;
            }

            if (MasterKey != null) {
                var result = MasterKey.Call ("CanUseLockedEntity", player, lockItem);
                if (result is bool) {
                    return null;
                }
            }

            BankProfile bank;
            if (lockItem is KeyLock && banks.TryGetValue (player.userID, out bank) && bank.items != null) {
                KeyLock keyLock = (KeyLock)lockItem;

                List<int> codes = bank.items.Select (profile => profile.dataInt).Where (dataInt => dataInt != 0).ToList ();
                //foreach(ItemProfile profile in bank.items) {
                //    if (profile.dataInt != 0) {
                //        codes.Add(profile.dataInt);
                //    }
                //}

                if (!keyLock.IsLocked ()) {
                    return null;
                }

                if (keyLock.HasLockPermission (player)) {
                    return null;
                }

                foreach (int code in codes) {
                    if (code == keyLock.keyCode) {
                        return true;
                    }
                }

                return false;
            }

            return null;
        }

        void OnPlayerConnected (BasePlayer player)
        {
            onlinePlayers [player].View = null;
            onlinePlayers [player].Target = null;
            onlinePlayers [player].Matches = null;

            if (!LoadProfile (player.userID)) {
                if (!banks.ContainsKey (player.userID)) {
                    banks.Add (player.userID, new BankProfile (player));
                }
            }
        }

        void OnPlayerDisconnected (BasePlayer player)
        {
            if (onlinePlayers [player].View != null) {
                ShowBank (player, onlinePlayers [player].View);
                SaveProfile (player.userID);
            }
        }

        void OnPlayerLootEnd (PlayerLoot inventory)
        {
            BasePlayer player;
            if ((player = inventory.GetComponent<BasePlayer> ()) == null)
                return;

            if (onlinePlayers.ContainsKey (player) && onlinePlayers [player].View != null) {
                if (onlinePlayers [player].View == inventory.entitySource) {
                    CloseBank (player, (StorageContainer)inventory.entitySource);
                }
            }
        }

        //void OnItemAddedToContainer(ItemContainer container, Item item)
        //{
        //    BasePlayer player = container.playerOwner;
        //    OnlinePlayer onlinePlayer;
        //    if (player != null && onlinePlayers.TryGetValue(player, out onlinePlayer)) {
        //        if (onlinePlayer.View != null && onlinePlayer.View.inventory == container) {
        //            int validAmount = GetValidAmount(item, container);

        //            if (validAmount <= 0) {
        //                item.MoveToContainer(container.playerOwner.inventory.containerMain);
        //                return;
        //            }

        //            if (validAmount < item.amount) {
        //                Item splitItem = item.SplitItem(item.amount - validAmount);

        //                splitItem.MoveToContainer(container.playerOwner.inventory.containerMain);
        //            }
        //        }
        //    }
        //}

        #endregion

        #region Commands

        [ChatCommand ("viewbank")]
        void ViewBank (BasePlayer player, string command, string [] args)
        {
            if (!IsAllowed (player)) return;

            if (args.Length < 1) {
                return;
            }

            var name = args [0];
            var ply = onlinePlayers [player];
            if (name == "list") {
                if (ply.Matches == null) {
                    return;
                }

                if (args.Length == 1) {
                    ShowMatchingPlayers (player);
                    return;
                }

                int index;
                if (!int.TryParse (args [1], out index)) {
                    return;
                }

                if (index > ply.Matches.Count) { } else
                    ShowBank (player, ply.Matches [index - 1]);

                return;
            }

            var matches = FindPlayersByName (name);
            if (matches.Count < 1) {
                return;
            }

            if (matches.Count > 1) {
                ply.Matches = matches;
                ShowMatchingPlayers (player);
                return;
            }

            ShowBank (player, matches [0]);
        }

        [ConsoleCommand ("bank")]
        void ccBank (ConsoleSystem.Arg arg)
        {
            cmdBank (arg.Connection.player as BasePlayer, arg.cmd.Name, null);
        }

        [ChatCommand ("bank")]
        void cmdBank (BasePlayer player, string command, string [] args)
        {
            if (npconly) return;

            ShowBank (player, player);
        }

        #endregion

        #region Core methods

        ItemLimit GetItemLimit (Item item)
        {
            object limit;
            if (itemLimits.TryGetValue (item.info.shortname, out limit) && limit is JObject) {
                return ((JObject)limit).ToObject<ItemLimit> ();
            }

            return null;
        }

        //int GetValidAmount(Item item, ItemContainer container) {
        //    ItemLimit limit;
        //    int totalAmount = container.GetAmount(item.info.itemid, false) - item.amount;

        //    if (itemLimits.TryGetValue(item.info.shortname, out limit)) {
        //        if (!limit.enabled) {
        //            return 0;
        //        }

        //        if (totalAmount < limit.minimum) {
        //            return 0;
        //        }

        //        if (totalAmount == 0 && item.amount > limit.maximum) {
        //            return limit.maximum;
        //        }

        //        if (totalAmount > limit.maximum) {
        //            return totalAmount - limit.maximum;
        //        }

        //        if (totalAmount == limit.maximum) {
        //            return 0;
        //        }
        //    }

        //    return item.amount;
        //}

        bool CanPlayerBank (BasePlayer player)
        {
            if (player.IsAdmin) {
                return true;
            }

            if (!permission.UserHasPermission (player.UserIDString, "bank.use")) {
                SendReply (player, GetMsg ("Denied: Permission", player));
                return false;
            }

            if (!player.CanBuild () && !npconly) {
                SendReply (player, GetMsg ("Denied: Privilege", player));
                return false;
            }

            if (radiationMax > 0 && player.radiationLevel > radiationMax) {
                SendReply (player, GetMsg ("Denied: Irradiated", player));
                return false;
            }

            if (player.IsSwimming ()) {
                SendReply (player, GetMsg ("Denied: Swimming", player));
                return false;
            }

            if (!player.IsOnGround () || player.IsFlying || player.isInAir) {
                SendReply (player, GetMsg ("Denied: Falling", player));
                return false;
            }

            if (player.isMounted) {
                SendReply (player, GetMsg ("Denied: Mounted", player));
                return false;
            }

            if (player.IsWounded ()) {
                SendReply (player, GetMsg ("Denied: Wounded", player));
                return false;
            }

            if (player.GetComponentInParent<CargoShip> ()) {
                SendReply (player, GetMsg ("Denied: Ship", player));
                return false;
            }

            if (player.GetComponentInParent<HotAirBalloon> ()) {
                SendReply (player, GetMsg ("Denied: Balloon", player));
                return false;
            }

            if (player.GetComponentInParent<Lift> ()) {
                SendReply (player, GetMsg ("Denied: Lift", player));
                return false;
            }

            if (!allowSafeZone && player.InSafeZone ()) {
                SendReply (player, GetMsg ("Denied: Safe Zone", player));
                return false;
            }

            var canBank = Interface.Call ("CanBank", player);
            if (canBank != null) {
                if (canBank is string) {
                    SendReply (player, Convert.ToString (canBank));
                } else {
                    SendReply (player, GetMsg ("Denied: Generic", player));
                }
                return false;
            }

            return true;
        }

        void ShowBank (BasePlayer player, BaseEntity target)
        {
            if (!CanPlayerBank (player))
                return;

            string playerID = player.userID.ToString ();

            if (cooldownMinutes > 0 && player.net.connection.authLevel < 1) {
                if (bankCooldowns.ContainsKey (playerID)) {
                    DateTime startTime = bankCooldowns [playerID];
                    DateTime endTime = DateTime.Now;

                    TimeSpan span = endTime.Subtract (startTime);
                    if (span.TotalMinutes > 0 && span.TotalMinutes < Convert.ToDouble (cooldownMinutes)) {
                        double timeleft = System.Math.Round (Convert.ToDouble (cooldownMinutes) - span.TotalMinutes, 2);
                        if (timeleft < 1) {
                            double timelefts = System.Math.Round ((Convert.ToDouble (cooldownMinutes) * 60) - span.TotalSeconds);
                            SendReply (player, string.Format (GetMsg ("Cooldown: Seconds", player), timelefts.ToString ()));
                        } else {
                            SendReply (player, string.Format (GetMsg ("Cooldown: Minutes", player), System.Math.Round (timeleft).ToString ()));
                        }
                        return;
                    } else {
                        bankCooldowns.Remove (playerID);
                    }
                }
            }

            if (!LoadProfile (player.userID) && !banks.ContainsKey (player.userID)) {
                banks.Add (player.userID, new BankProfile (player));
            }

            if (!bankCooldowns.ContainsKey (player.userID.ToString ()) && player.net.connection.authLevel < 1) {
                bankCooldowns.Add (playerID, DateTime.Now);
            }
            var ply = onlinePlayers [player];
            if (ply.View == null) {
#if DEBUG
                Puts ($"Opening bank {player.userID}");
#endif
                OpenBank (player, target);
                return;
            }

			// Player will need to close bank (Esc) before opening again
			//  cause issue when spamming /bank command using key bind 
			
            //CloseBank (player, ply.View);
            //timer.In (1f, () => OpenBank (player, target));
        }

        void HideBank (BasePlayer player)
        {
            player.EndLooting ();
            var ply = onlinePlayers [player];
            if (ply.View == null) {
                return;
            }

            CloseBank (player, ply.View);
        }

        string GetBox (BasePlayer player)
        {
            foreach (KeyValuePair<string, object> kvp in boxPrefabs) {
                if (permission.UserHasPermission (player.UserIDString, kvp.Key)) {
                    return kvp.Value.ToString ();
                }
            }

            return defaultBoxPrefab;
        }

        int GetSlots (BasePlayer player)
        {
            var availableSlots = new List<int> ();
            availableSlots.Add (defaultSlots);

            foreach (KeyValuePair<string, object> kvp in boxSlots) {
                if (permission.UserHasPermission (player.UserIDString, kvp.Key)) {
                    availableSlots.Add (Convert.ToInt32 (kvp.Value));
                }
            }

            return availableSlots.Max ();
        }

        void OpenBank (BasePlayer player, BaseEntity targArg)
        {
            Subscribe (nameof (OnEntityTakeDamage));
            var pos = new Vector3 (player.transform.position.x, player.transform.position.y - 1, player.transform.position.z);
            string box = GetBox (player);

            int slots = GetSlots (player);

            var view = GameManager.server.CreateEntity (box, pos) as StorageContainer;
            view.SetFlag(BaseEntity.Flags.Disabled, true);

            var groundWatch = view.GetComponent<GroundWatch>();
            if (groundWatch != null)
            {
                UnityEngine.Object.Destroy(groundWatch);
                groundWatch.fails = int.MinValue;
            }

            UnityEngine.Object.Destroy(view.GetComponent<DestroyOnGroundMissing>());

            foreach (Collider collider in view.GetComponentsInChildren<Collider>())
            {
                UnityEngine.Object.DestroyImmediate(collider);
            }
						
            view.limitNetworking = true;
            view.SetParent(player, true, true);

            if (!view) return;
            
            view.transform.position = pos;
            view.OwnerID = player.userID;

            player.EndLooting ();
            if (targArg is BasePlayer) {
                BasePlayer target = targArg as BasePlayer;
                BankProfile profile = banks [target.userID];
                view.inventorySlots = slots;
                // use CreateInventory instead of _inventory directly because 
                // CreateInventory and DoServerDestroy use the Pool function for alloc and free
                view.CreateInventory(true);
                ItemContainer bank = profile.SetContainer (view.inventory, target, slots);
                if (!containers.ContainsKey (bank)) {
                    containers.Add (bank, player.userID);
                }
                view.enableSaving = false;
                view.Spawn ();
                view.inventory.entityOwner = view;

                profile.open = true;
                onlinePlayers [player].View = view;
                onlinePlayers [player].Target = target;

                timer.In (0.1f, delegate () {
                    view.PlayerOpenLoot (player);
                });
            }
        }

        void CloseBank (BasePlayer player, StorageContainer view)
        {
            if (!onlinePlayers.ContainsKey (player)) return;
            if (onlinePlayers [player].View == null) return;

            if (!banks.ContainsKey (player.userID)) {
                return;
            }

            BankProfile profile = banks [player.userID];

            InvalidateBank (player, profile, view);

            profile.items.Clear ();
            foreach (Item item in view.inventory.itemList) {
                profile.Add (item);
            }

            SaveProfile (player.userID, profile);

            foreach (Item item in view.inventory.itemList.ToArray ()) {
                if (item.position != -1) {
                    item.RemoveFromContainer ();
                    item.Remove (0f);
                }
            }

            profile.open = false;

            if (containers.ContainsKey (view.inventory)) {
                containers.Remove (view.inventory);
            }

            player.inventory.loot.containers = new List<ItemContainer> ();
            // seems to be a bug to just overwrite _inventory variable
            // without destroying current value DoServerDestry will take care of it
            //view._inventory = new ItemContainer();           

            if (player.inventory.loot.IsLooting ()) {
                player.SendConsoleCommand ("inventory.endloot", null);
            }


            onlinePlayers [player].View = null;
            onlinePlayers [player].Target = null;
            view.Kill(BaseNetworkable.DestroyMode.None);

#if DEBUG
            Puts($"Closing Bank {player.userID},  view: {onlinePlayers.Values.Count(p => p.View != null)}");
#endif

            timer.In (1f, delegate () {
                if (onlinePlayers.Values.Count (p => p.View != null) <= 0) {
                    Unsubscribe (nameof (OnEntityTakeDamage));
                }
            });
        }

        void InvalidateBank (BasePlayer player, BankProfile profile, StorageContainer view)
        {
            bool returned = false;
            if (view != null && view.inventory != null && view.inventory.itemList != null) {
                foreach (var item in view.inventory.itemList.ToArray ()) {
                    ItemLimit limit = GetItemLimit (item);

                    if (limit != null && limit.enabled) {
                        if (item.amount < limit.minimum) {
                            returned = true;
                            if (player.inventory.containerMain == null || (player.inventory.containerMain != null && !item.MoveToContainer (player.inventory.containerMain))) {
                                if (player.inventory.containerBelt == null || (player.inventory.containerBelt != null && !item.MoveToContainer (player.inventory.containerBelt))) {
                                    item.Drop (player.eyes.HeadForward () * 2, default (Vector3));
                                }
                            }
                            continue;
                        }

                        if (item.amount > limit.maximum) {
                            returned = true;
                            Item invalidItem;

                            if (limit.maximum > 0) {
                                invalidItem = item.SplitItem (item.amount - limit.maximum);
                            } else {
                                invalidItem = item;
                            }
                            if (invalidItem != null) {
                                if (player.inventory.containerMain == null || (player.inventory.containerMain != null && !invalidItem.MoveToContainer (player.inventory.containerMain))) {
                                    if (player.inventory.containerBelt == null || (player.inventory.containerBelt != null && !invalidItem.MoveToContainer (player.inventory.containerBelt))) {
                                        invalidItem.Drop (player.eyes.HeadForward () * 2, default (Vector3));
                                    }
                                }
                            }
                        }
                    }
                }
            }

            if (returned) {
                SendReply (player, GetMsg ("Limit: Return", player));
            }
        }

        #endregion

        #region HelpText
        void SendHelpText (BasePlayer player)
        {
            var sb = new StringBuilder ()
               .Append ("Bank by <color=#ce422b>Calytic</color>\n")
               .Append ("  ").Append ("<color=\"#ffd479\">/bank</color> - Open your bank box").Append ("\n");

            if (player.IsAdmin) {
                sb.Append ("  ").Append ("<color=\"#ffd479\">/viewbank \"Player Name\"</color> - View any players bank").Append ("\n");
            }
            player.ChatMessage (sb.ToString ());
        }
        #endregion

        #region Helper methods

        string GetMsg (string key, BasePlayer player = null)
        {
            return lang.GetMessage (key, this, player == null ? null : player.UserIDString);
        }

        List<BasePlayer> FindPlayersByName (string name)
        {
            List<BasePlayer> matches = new List<BasePlayer> ();

            foreach (var ply in BasePlayer.activePlayerList) {
                if (ply.displayName.ToLower ().Contains (name.ToLower ()))
                    matches.Add (ply);
            }

            foreach (var ply in BasePlayer.sleepingPlayerList) {
                if (ply.displayName.ToLower ().Contains (name.ToLower ()))
                    matches.Add (ply);
            }

            return matches;
        }

        void ShowMatchingPlayers (BasePlayer player)
        {
            int i = 0;
            foreach (var ply in onlinePlayers [player].Matches) {
                i++;
                player.ChatMessage ($"{i} - {ply.displayName} ({ply.userID})");
            }
        }

        bool IsAllowed (BasePlayer player)
        {
            if (player.IsAdmin) return true;
            SendReply (player, GetMsg ("Denied: Permission", player));
            return false;
        }

        T GetConfig<T> (string name, string name2, T defaultValue)
        {
            if (Config [name, name2] == null) {
                return defaultValue;
            }

            return (T)Convert.ChangeType (Config [name, name2], typeof (T));
        }

        T GetConfig<T> (string name, T defaultValue)
        {
            if (Config [name] == null) {
                return defaultValue;
            }

            return (T)Convert.ChangeType (Config [name], typeof (T));
        }

        #endregion
    }
}