﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Text;
using Facepunch;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins;

[Info("Frame Box", "MJSU", "1.0.3")]
[Description("Deploys a frame on a box allowing the player to set the image")]
internal class FrameBox : RustPlugin
{
    #region Class Fields
    private StoredData _storedData; //Plugin Data
    private PluginConfig _pluginConfig; //Plugin Config

    private static FrameBox _ins;

    private const string UsePermission = "framebox.use";
    private const string NoCostPermission = "framebox.nocost";
    private const string AccentColor = "#de8732";

    private readonly Hash<NetworkableId, BoxData> _boxes = new();
    private readonly Hash<NetworkableId, FrameData> _frames = new();
    private readonly Hash<string, ContainerData> _containerPositions = new();
    #endregion

    #region Setup & Loading
    private void Init()
    {
        _ins = this;
        _storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);

        permission.RegisterPermission(UsePermission, this);
        permission.RegisterPermission(NoCostPermission, this);

        cmd.AddChatCommand(_pluginConfig.ChatCommand, this, SignBoxChatCommand);
    }

    protected override void LoadDefaultMessages()
    {
        lang.RegisterMessages(new Dictionary<string, string>
        {
            [LangKeys.Chat] = $"<color=#bebebe>[<color={AccentColor}>{Title}</color>] {{0}}</color>",
            [LangKeys.NoPermission] = "You do not have permission to use this command",
            [LangKeys.MissingItem] = "{0}: {1}x",
            [LangKeys.MissingMessage] =
                "You do not have enough items to add a frame to this box. You're missing:\n{0}",
            [LangKeys.CostPre] = "Cost to make:\n",
            [LangKeys.Cost] = "{0}: {1}x",
            [LangKeys.AddInvalidSyntax] =
                $"Invalid Syntax: <color={AccentColor}>/{{0}} {{2}} front</color> - to add the frame to the front of the box" +
                "\nAvailable frame positions for this container are {1}",
            [LangKeys.InvalidAddFramePosition] = "Invalid Frame Position. Available frame positions are:\n {0}",
            [LangKeys.NotLookingAt] = "You're not looking at a box or frame",
            [LangKeys.AddSlotIsTaken] = "The frame position {0} is already taken. Open positions are:\n{1}",
            [LangKeys.AddSuccess] = "You have successfully added a frame onto your box in position {0}. " +
                                    "In order to access the box using the frame you need to first lock the frame." +
                                    "The only way to unlock the frame is using the /fb unlock command",
            [LangKeys.RemoveSuccess] = "You have successfully removed the frame from position {0}",
            [LangKeys.UnlockBuildingBlocked] = "Cannot unlock in a building blocked zone",
            [LangKeys.UnlockSuccess] = "You have successfully unlocked the sign.",
            [LangKeys.NotLookingAtFrame] = "You're not looking at a frame.",
            [LangKeys.NotLookingAtFrameBoxFrame] = "You're not looking at a frame box frame",
            [LangKeys.NotLookingAtFrameBox] = "You're not looking at a frame box or frame box frame",
            [LangKeys.NoFrameInPosition] = "There is no frame in position {0}",
            [LangKeys.ContainerNotAllowed] = "This container is not allowed to have a frame on it.",
            [LangKeys.NoUnlockPermission] = "You do not have permission to unlock this sign.",
            [LangKeys.AddSubCommand] = "add",
            [LangKeys.RemoveSubCommand] = "remove",
            [LangKeys.CostSubCommand] = "cost",
            [LangKeys.UnlockSubCommand] = "unlock",
            [LangKeys.AllSubOption] = "all",
            [LangKeys.HelpText] =
                "Allows placing photo frames on boxes. After settings the sign and locking it you can now access the box while looking at the sign.\n" +
                $"<color={AccentColor}>/{{0}} {{1}}</color> to see the available positions to palace a sign\n" +
                $"<color={AccentColor}>/{{0}} {{1}} right</color> to place a sign on the right\n" +
                $"<color={AccentColor}>/{{0}} {{1}} frontleft</color> to place a sign on the front left\n" +
                $"<color={AccentColor}>/{{0}} {{2}}</color> to remove a sign you're looking at\n" +
                $"<color={AccentColor}>/{{0}} {{3}}</color> to get the cost of the sign\n" +
                $"<color={AccentColor}>/{{0}} {{4}}</color> to unlock the sign to allow edits again\n" +
                $"<color={AccentColor}>/{{0}}</color> to see this help text again\n"
        }, this);
    }

    protected override void LoadDefaultConfig()
    {
        PrintWarning("Loading Default Config");
    }

    protected override void LoadConfig()
    {
        base.LoadConfig();
        Config.Settings.DefaultValueHandling = DefaultValueHandling.Populate;
        _pluginConfig = AdditionalConfig(Config.ReadObject<PluginConfig>());
        Config.WriteObject(_pluginConfig);
    }

    //TODO: Fix config back to normal
    public PluginConfig AdditionalConfig(PluginConfig config)
    {
        config.Cost = config.Cost ?? new Hash<string, int>
        {
            ["wood"] = 150
        };

        config.SignPositions = config.SignPositions ?? new Hash<string, ContainerData>
        {
            ["box.wooden.large"] = new()
            {
                ItemShortname = "photoframe.portrait",
                PositionData = new Hash<string, PositionData>
                {
                    ["Left"] = new()
                    {
                        Offset = new Vector3(0.95f, 0.4f, 0),
                        Rotation = new Vector3(0, 90, 90),
                        IncludeAll = true
                    },
                    ["Right"] = new()
                    {
                        Offset = new Vector3(-0.95f, 0.4f, 0),
                        Rotation = new Vector3(0, 270, 90),
                        IncludeAll = true
                    },
                    ["Front"] = new()
                    {
                        Offset = new Vector3(0, 0.4f, 0.55f),
                        Rotation = new Vector3(0, 0, 90),
                        IncludeAll = false
                    },
                    ["FrontRight"] = new()
                    {
                        Offset = new Vector3(-0.45f, 0.4f, 0.55f),
                        Rotation = new Vector3(0, 0, 90),
                        IncludeAll = true
                    },
                    ["FrontLeft"] = new()
                    {
                        Offset = new Vector3(0.45f, 0.4f, 0.55f),
                        Rotation = new Vector3(0, 0, 90),
                        IncludeAll = true
                    },
                    ["Back"] = new()
                    {
                        Offset = new Vector3(0, 0.4f, -0.55f),
                        Rotation = new Vector3(0, 180, 90),
                        IncludeAll = false
                    },
                    ["BackRight"] = new()
                    {
                        Offset = new Vector3(-0.45f, 0.4f, -0.55f),
                        Rotation = new Vector3(0, 180, 90),
                        IncludeAll = true
                    },
                    ["BackLeft"] = new()
                    {
                        Offset = new Vector3(0.45f, 0.4f, -0.55f),
                        Rotation = new Vector3(0, 180, 90),
                        IncludeAll = true
                    },
                    ["Top"] = new()
                    {
                        Offset = new Vector3(0, 0.775f, 0),
                        Rotation = new Vector3(270, 0, 90),
                        IncludeAll = false
                    },
                    ["TopRight"] = new()
                    {
                        Offset = new Vector3(-0.45f, 0.775f, 0),
                        Rotation = new Vector3(270, 0, 90),
                        IncludeAll = true
                    },
                    ["TopLeft"] = new()
                    {
                        Offset = new Vector3(0.45f, 0.775f, 0),
                        Rotation = new Vector3(270, 0, 90),
                        IncludeAll = true
                    }
                }
            },
            ["box.wooden"] = new()
            {
                ItemShortname = "photoframe.portrait",
                PositionData = new Hash<string, PositionData>
                {
                    ["Left"] = new()
                    {
                        Offset = new Vector3(0.49f, 0.35f, 0),
                        Rotation = new Vector3(0, 90, 90),
                        IncludeAll = true
                    },
                    ["Right"] = new()
                    {
                        Offset = new Vector3(-0.49f, 0.35f, 0),
                        Rotation = new Vector3(0, 270, 90),
                        IncludeAll = true
                    },
                    ["Front"] = new()
                    {
                        Offset = new Vector3(0, 0.35f, .39f),
                        Rotation = new Vector3(0, 0, 90),
                        IncludeAll = true
                    },
                    ["Back"] = new()
                    {
                        Offset = new Vector3(0, 0.35f, -.39f),
                        Rotation = new Vector3(0, 180, 90),
                        IncludeAll = true
                    },
                    ["Top"] = new()
                    {
                        Offset = new Vector3(0, 0.6f, 0),
                        Rotation = new Vector3(270, 0, 90),
                        IncludeAll = true
                    }
                }
            }
        };
        return config;
    }

    private void OnServerInitialized()
    {
        foreach (KeyValuePair<string, ContainerData> entityPositions in _pluginConfig.SignPositions)
        {
            string containerShortname = GetShortPrefabName(entityPositions.Key);
            if (string.IsNullOrEmpty(containerShortname))
            {
                continue;
            }

            string prefabName = GetPrefabName(entityPositions.Value.ItemShortname);
            if (string.IsNullOrEmpty(prefabName))
            {
                continue;
            }

            Hash<string, PositionData> positions = new();
            foreach (KeyValuePair<string, PositionData> position in entityPositions.Value.PositionData)
            {
                PositionData data = position.Value;
                data.DisplayName = position.Key;
                positions[position.Key.ToLower()] = data;
            }

            ContainerData containerData = new()
            {
                ItemShortname = prefabName,
                PositionData = positions
            };

            _containerPositions[containerShortname] = containerData;
        }

        foreach (KeyValuePair<ulong, Hash<string, ulong>> data in _storedData.FrameData.ToList())
        {
            BoxStorage box = BaseNetworkable.serverEntities.Find(new NetworkableId(data.Key)) as BoxStorage;
            if (box == null)
            {
                _storedData.FrameData.Remove(data.Key);
                continue;
            }

            BoxData boxData = new(box);

            foreach (KeyValuePair<string, ulong> framePos in data.Value.ToList())
            {
                PhotoFrame frame = BaseNetworkable.serverEntities.Find(new NetworkableId(framePos.Value)) as PhotoFrame;
                if (frame == null)
                {
                    data.Value.Remove(framePos.Key);
                    continue;
                }

                if (!boxData.AddFrame(frame, framePos.Key))
                {
                    data.Value.Remove(framePos.Key);
                    frame.Kill();
                }
            }
        }

        foreach (BasePlayer player in BasePlayer.activePlayerList)
        {
            OnPlayerConnected(player);
        }
    }

    private void OnNewSave(string filename)
    {
        _storedData = new StoredData();
        SaveData();
    }

    private void OnPlayerConnected(BasePlayer player)
    {
        NextTick(() =>
        {
            if (player.IsAdmin || player.IsDeveloper)
            {
                player.gameObject.AddComponent<AdminBoxBehavior>();
            }
        });
    }

    private void Unload()
    {
        foreach (AdminBoxBehavior adminBoxBehavior in GameObject.FindObjectsOfType<AdminBoxBehavior>())
        {
            adminBoxBehavior.DoDestroy();
        }

        SaveData();
        _ins = null;
    }
    #endregion

    #region Chat Command
    private void SignBoxChatCommand(BasePlayer player, string cmd, string[] args)
    {
        if (!HasPermission(player, UsePermission))
        {
            Chat(player, LangKeys.NoPermission);
            return;
        }

        if (args.Length == 0)
        {
            HandleHelp(player);
            return;
        }

        string subCommand = args[0];
        if (subCommand.Equals(Lang(LangKeys.AddSubCommand, player), StringComparison.InvariantCultureIgnoreCase))
        {
            HandleAdd(player, args);
        }
        else if (subCommand.Equals(Lang(LangKeys.RemoveSubCommand, player), StringComparison.InvariantCultureIgnoreCase))
        {
            HandleRemove(player, args);
        }
        else if (subCommand.Equals(Lang(LangKeys.CostSubCommand, player), StringComparison.InvariantCultureIgnoreCase))
        {
            HandleCost(player);
        }
        else if (subCommand.Equals(Lang(LangKeys.UnlockSubCommand, player), StringComparison.InvariantCultureIgnoreCase))
        {
            HandleUnlock(player, args);
        }
        else
        {
            HandleHelp(player);
        }
    }

    public void HandleHelp(BasePlayer player)
    {
        Chat(player, LangKeys.HelpText, _pluginConfig.ChatCommand,
            Lang(LangKeys.AddSubCommand, player),
            Lang(LangKeys.RemoveSubCommand, player),
            Lang(LangKeys.CostSubCommand, player),
            Lang(LangKeys.UnlockSubCommand, player));
    }

    public void HandleAdd(BasePlayer player, string[] args)
    {
        BaseCombatEntity box = Raycast<IItemContainerEntity>(player) as BaseCombatEntity;
        if (box == null)
        {
            Chat(player, LangKeys.NotLookingAt);
            return;
        }

        if (box is PhotoFrame)
        {
            FrameData data = _frames[box.net.ID];
            if (data == null)
            {
                Chat(player, LangKeys.NotLookingAt);
                return;
            }

            box = data.BoxData.Box;
        }

        ContainerData containerData = _containerPositions[box.ShortPrefabName];
        if (containerData == null)
        {
            Chat(player, LangKeys.ContainerNotAllowed);
            return;
        }

        if (args.Length < 2)
        {
            Chat(player, LangKeys.AddInvalidSyntax, _pluginConfig.ChatCommand, string.Join(", ", containerData.PositionData.Values.Select(cd => cd.DisplayName).ToArray()), Lang(LangKeys.AddSubCommand, player));
            return;
        }

        string pos = args[1].ToLower();
        if (!containerData.PositionData.ContainsKey(pos) && !pos.Equals(Lang(LangKeys.AllSubOption, player), StringComparison.InvariantCultureIgnoreCase))
        {
            Chat(player, LangKeys.InvalidAddFramePosition, string.Join(", ", containerData.PositionData.Values.Select(cd => cd.DisplayName).ToArray()));
            return;
        }

        BoxData boxData = _boxes[box.net.ID] ?? new BoxData(box);
        if (boxData.IsSlotTaken(pos))
        {
            Chat(player, LangKeys.AddSlotIsTaken, pos, string.Join(", ", boxData.GetOpenPositions(false)));
            return;
        }

        Hash<string, ulong> frameData = _storedData.FrameData[box.net.ID.Value];
        if (frameData == null)
        {
            frameData = new Hash<string, ulong>();
            _storedData.FrameData[box.net.ID.Value] = frameData;
        }

        if (pos.Equals(Lang(LangKeys.AllSubOption, player), StringComparison.InvariantCultureIgnoreCase))
        {
            if (!HasPermission(player, NoCostPermission) && !CanAfford(player, _pluginConfig.Cost, boxData.GetOpenPositions(true).Length))
            {
                return;
            }

            foreach (KeyValuePair<string, PositionData> positionData in containerData.PositionData)
            {
                if (!positionData.Value.IncludeAll)
                {
                    continue;
                }

                frameData[positionData.Key] = AddFrame(boxData, containerData.ItemShortname, positionData.Value, positionData.Key);
            }
        }
        else
        {
            if (!HasPermission(player, NoCostPermission) && !CanAfford(player, _pluginConfig.Cost, 1))
            {
                return;
            }

            PositionData framePos = containerData.PositionData[pos];
            frameData[pos] = AddFrame(boxData, containerData.ItemShortname, framePos, pos);
        }

        Chat(player, LangKeys.AddSuccess, pos);
        SaveData();
    }

    public ulong AddFrame(BoxData boxData, string prefabName, PositionData framePos, string pos)
    {
        PhotoFrame frame = GameManager.server.CreateEntity(prefabName, boxData.Box.transform.TransformPoint(framePos.Offset), boxData.Box.transform.rotation * Quaternion.Euler(framePos.Rotation)) as PhotoFrame;
        frame.Spawn();
        boxData.AddFrame(frame, pos);
        return frame.net.ID.Value;
    }

    public void HandleRemove(BasePlayer player, string[] args)
    {
        BoxData boxData;
        FrameData frameData;

        if (!GetFrameBox(player, out boxData, out frameData))
        {
            return;
        }

        string[] framesToRemove = null;
        if (args.Length == 1)
        {
            if (frameData == null)
            {
                Chat(player, LangKeys.NotLookingAtFrame);
                return;
            }

            framesToRemove = new [] {frameData.Position};
        }
        else if(args.Length > 1)
        {
            framesToRemove = ParseFramePositions(player, boxData, args[1].ToLower());
        }

        if (framesToRemove == null)
        {
            return;
        }

        int removed = 0;
        foreach (string pos in framesToRemove)
        {
            if (boxData.RemoveFrame(pos, true))
            {
                removed++;
            }
        }

        if (removed == 0)
        {
            Chat(player, LangKeys.NoFrameInPosition, args[1].ToLower());
            return;
        }

        if (_pluginConfig.RefundOnRemove)
        {
            foreach (KeyValuePair<string, int> cost in _pluginConfig.Cost)
            {
                int amount = Mathf.FloorToInt(cost.Value * _pluginConfig.RefundPercentage * removed);
                if (amount <= 0)
                {
                    continue;
                }

                Item item = ItemManager.CreateByName(cost.Key, amount);
                player.GiveItem(item);
            }
        }

        Chat(player, LangKeys.RemoveSuccess, args[1]);
        SaveData();
    }

    public void HandleCost(BasePlayer player)
    {
        StringBuilder sb = new(Lang(LangKeys.CostPre, player));

        foreach (KeyValuePair<string, int> item in _pluginConfig.Cost)
        {
            sb.AppendLine(Lang(LangKeys.Cost, player, ItemManager.FindItemDefinition(item.Key)?.displayName.translated, item.Value));
        }

        Chat(player, sb.ToString());
    }

    public void HandleUnlock(BasePlayer player, string[] args)
    {
        BuildingPrivlidge priv = player.GetBuildingPrivilege();
        if (priv != null && !priv.IsAuthed(player))
        {
            Chat(player, LangKeys.UnlockBuildingBlocked);
            return;
        }

        BoxData boxData;
        FrameData frameData;

        if (!GetFrameBox(player, out boxData, out frameData))
        {
            return;
        }

        string[] framesToUnlock = null;
        if (args.Length == 1)
        {
            if (frameData == null)
            {
                Chat(player, LangKeys.NotLookingAtFrame);
                return;
            }

            framesToUnlock = new [] {frameData.Position};
        }
        else if(args.Length > 1)
        {
            framesToUnlock = ParseFramePositions(player, boxData, args[1].ToLower());
        }

        if (framesToUnlock == null)
        {
            return;
        }

        int unlocked = 0;
        foreach (string frame in framesToUnlock)
        {
            if (boxData.GetFrame(frame)?.Unlock(player) ?? false)
            {
                unlocked++;
            }
        }

        if (unlocked == 0)
        {
            Chat(player, LangKeys.NoUnlockPermission);
            return;
        }


        Chat(player, LangKeys.UnlockSuccess);
    }

    private bool GetFrameBox(BasePlayer player, out BoxData boxData, out FrameData frameData)
    {
        boxData = null;
        frameData = null;

        BaseCombatEntity box = Raycast<IItemContainerEntity>(player) as BaseCombatEntity;
        if (box == null)
        {
            Chat(player, LangKeys.NotLookingAt);
            return false;
        }

        if (box is PhotoFrame)
        {
            frameData = _frames[box.net.ID];
            if (frameData == null)
            {
                Chat(player, LangKeys.NotLookingAtFrameBoxFrame);
                return false;
            }

            boxData = frameData.BoxData;
        }
        else
        {
            boxData = _boxes[box.net.ID];
            if (boxData == null)
            {
                Chat(player, LangKeys.NotLookingAtFrameBox);
                return false;
            }
        }

        return true;
    }

    public string[] ParseFramePositions(BasePlayer player, BoxData box, string pos)
    {
        if (pos.Equals(Lang(LangKeys.AllSubOption, player)))
        {
            return box.GetTakenFramePositions();
        }

        FrameData frameData = box.GetFrame(pos);
        if (frameData == null)
        {
            Chat(player, LangKeys.NoFrameInPosition, pos);
            return null;
        }

        return new []{pos};
    }
    #endregion

    #region Hooks
    private object CanLootEntity(BasePlayer player, PhotoFrame frame)
    {
        if (frame == null || !frame.IsLocked())
        {
            return null;
        }

        IItemContainerEntity box = _frames[frame.net.ID]?.BoxData.Box as IItemContainerEntity;
        if (box == null)
        {
            return null;
        }

        box.PlayerOpenLoot(player);
        return true;
    }

    private void OnEntityKill(BoxStorage box)
    {
        if (box == null)
        {
            PrintError($"Box is null???\n{Environment.StackTrace}");
        }

        _boxes[box.net.ID]?.OnKilled();
    }

    private void OnEntityKill(PhotoFrame frame)
    {
        if (frame == null)
        {
            PrintError($"frame is null???\n{Environment.StackTrace}");
        }

        _frames[frame.net.ID]?.OnKilled();
    }

    private void OnSignLocked(PhotoFrame frame, BasePlayer player)
    {
        FrameData frameData = _frames[frame.net.ID];
        if (frameData == null)
        {
            return;
        }

        frame.OwnerID = 0;
    }
    #endregion

    #region Sign Artist Hooks
    private object GetImageRotation(PhotoFrame frame)
    {
        if (!_frames.ContainsKey(frame.net.ID))
        {
            return null;
        }

        return RotateFlipType.Rotate270FlipNone;
    }
    #endregion

    #region Magic Remove Hooks
    private Hash<string, int> GetRemoveAdditionalRefund(BoxStorage entity, Hash<string, int> refund)
    {
        BoxData box = _boxes[entity.net.ID];
        if (box == null)
        {
            return null;
        }

        int frameCount = box.GetFrameCount();
        foreach (KeyValuePair<string, int> item in _pluginConfig.Cost)
        {
            int amount = Mathf.FloorToInt(item.Value * frameCount * _pluginConfig.RefundPercentage);
            if (amount == 0)
            {
                continue;
            }

            refund[item.Key] += amount;
        }

        return refund;
    }
    #endregion

    #region Handle Paying
    public bool CanAfford(BasePlayer player, Hash<string, int> cost, int multiplier)
    {
        bool canAfford = true;
        StringBuilder missingItems = new();
        foreach (KeyValuePair<string, int> item in cost)
        {
            ItemDefinition def = ItemManager.FindItemDefinition(item.Key);
            int amount = player.inventory.GetAmount(def.itemid);
            if (amount < item.Value * multiplier)
            {
                string itemName = ItemManager.FindItemDefinition(item.Key).displayName.english;
                missingItems.AppendLine(Lang(LangKeys.MissingItem, player, itemName,
                    item.Value * multiplier - amount));
                canAfford = false;
            }
        }

        if (!canAfford)
        {
            Chat(player, Lang(LangKeys.MissingMessage, player, missingItems));
            return false;
        }

        TakeCost(player, cost, multiplier);

        return true;
    }

    public void TakeCost(BasePlayer player, Hash<string, int> cost, int multiplier)
    {
        List<Item> items = Pool.Get<List<Item>>();
        foreach (KeyValuePair<string, int> item in cost)
        {
            ItemDefinition def = ItemManager.FindItemDefinition(item.Key);
            player.inventory.Take(items, def.itemid, item.Value * multiplier);
            player.Command("note.inv", def.itemid, -item.Value * multiplier);
        }

        foreach (Item item in items)
        {
            item.Remove();
        }

        Pool.FreeUnmanaged(ref items);
    }
    #endregion

    #region Helper Methods
    public string GetShortPrefabName(string shortname)
    {
        ItemDefinition def = ItemManager.FindItemDefinition(shortname);
        if (def == null)
        {
            PrintWarning($"{shortname} is not a valid item shortname!");
            return null;
        }

        string prefabName = def.GetComponent<ItemModDeployable>()?.entityPrefab.Get().GetComponent<BaseEntity>()
            .ShortPrefabName;
        if (string.IsNullOrEmpty(prefabName))
        {
            PrintWarning($"{shortname} does not contain a deployable item and cannot be used");
            return null;
        }

        return prefabName;
    }

    public string GetPrefabName(string shortname)
    {
        ItemDefinition def = ItemManager.FindItemDefinition(shortname);
        if (def == null)
        {
            PrintWarning($"{shortname} is not a valid item shortname!");
            return null;
        }

        string prefabName = def.GetComponent<ItemModDeployable>()?.entityPrefab.Get().GetComponent<BaseEntity>()
            .PrefabName;
        if (string.IsNullOrEmpty(prefabName))
        {
            PrintWarning($"{shortname} does not contain a deployable item and cannot be used");
            return null;
        }

        return prefabName;
    }

    public T Raycast<T>(BasePlayer player) where T : class
    {
        RaycastHit[] hits = Physics.RaycastAll(player.eyes.HeadRay(), _pluginConfig.MaxDistance);
        GamePhysics.Sort(hits);
        foreach (RaycastHit hit in hits)
        {
            BaseEntity entity = hit.GetEntity();
            if (entity == null)
            {
                continue;
            }

            T type = entity as T;
            if (type == null)
            {
                continue;
            }

            if (Vector3.Distance(player.eyes.position, entity.transform.position) > _pluginConfig.MaxDistance + 3)
            {
                continue;
            }

            return type;
        }

        return default(T);
    }

    public void SaveData()
    {
        if (_storedData != null)
        {
            Interface.Oxide.DataFileSystem.WriteObject(Name, _storedData);
        }
    }

    public void Chat(BasePlayer player, string key, params object[] args) =>
        PrintToChat(player, Lang(LangKeys.Chat, player, Lang(key, player, args)));

    public string Lang(string key, BasePlayer player = null, params object[] args)
    {
        try
        {
            return string.Format(lang.GetMessage(key, this, player?.UserIDString), args);
        }
        catch (Exception ex)
        {
            PrintError($"Lang Key '{key}' threw exception:\n{ex}");
            throw;
        }
    }

    public bool HasPermission(BasePlayer player, string perm) =>
        permission.UserHasPermission(player.UserIDString, perm);
    #endregion

    #region Data Classes
    public class FrameData
    {
        public PhotoFrame Frame { get; }
        public BoxData BoxData { get; }
        public string Position { get; }

        public FrameData(PhotoFrame frame, BoxData boxData, string position)
        {
            Frame = frame;
            BoxData = boxData;
            Position = position;
            DestroyGroundWatch(Frame);
            _ins._frames[frame.net.ID] = this;
        }

        public bool Unlock(BasePlayer player)
        {
            if (Frame.OwnerID != 0 && !Frame.CanUnlockSign(player))
            {
                return false;
            }

            Frame.SetFlag(BaseEntity.Flags.Locked, false);
            Frame.SendNetworkUpdate();
            return true;
        }

        private void DestroyGroundWatch(BaseEntity entity)
        {
            DestroyOnGroundMissing missing = entity.GetComponent<DestroyOnGroundMissing>();
            if (missing != null)
            {
                GameObject.Destroy(missing);
            }

            GroundWatch watch = entity.GetComponent<GroundWatch>();
            if (watch != null)
            {
                GameObject.Destroy(watch);
            }
        }

        public void OnKilled()
        {
            BoxData.RemoveFrame(Position, false);
        }

        public void RemoveFrame(bool kill)
        {
            _ins._frames.Remove(Frame.net.ID);
            _ins._storedData.FrameData.Remove(Frame.net.ID.Value);
            if (kill)
            {
                Frame.Kill();
            }
        }
    }

    public class BoxData
    {
        public BaseCombatEntity Box { get; }
        private Hash<string, FrameData> BoxFrames { get; }

        public BoxData(BaseCombatEntity box)
        {
            Box = box;
            BoxFrames = new Hash<string, FrameData>();
            _ins._boxes[Box.net.ID] = this;
        }

        public FrameData GetFrame(string position)
        {
            return BoxFrames[position];
        }

        public bool AddFrame(PhotoFrame frame, string position)
        {
            if (IsSlotTaken(position))
            {
                return false;
            }

            BoxFrames[position] = new FrameData(frame, this, position);
            return true;
        }

        public bool RemoveFrame(string position, bool kill)
        {
            FrameData frame = BoxFrames[position];
            if (frame == null)
            {
                return false;
            }

            frame.RemoveFrame(kill);
            BoxFrames.Remove(position);

            Hash<string, ulong> data = _ins._storedData.FrameData[Box.net.ID.Value];
            if (data != null)
            {
                data.Remove(position);
                if (data.Count == 0)
                {
                    _ins._storedData.FrameData.Remove(Box.net.ID.Value);
                    _ins._boxes.Remove(Box.net.ID);
                }
            }

            return true;
        }

        public bool IsSlotTaken(string position)
        {
            return BoxFrames.ContainsKey(position);
        }

        public string[] GetOpenPositions(bool allOnly)
        {
            return _ins._containerPositions[Box.ShortPrefabName].PositionData
                .Where(fp => !BoxFrames.ContainsKey(fp.Key) && (!allOnly || fp.Value.IncludeAll))
                .Select(fp => fp.Value.DisplayName)
                .ToArray();
        }

        public string[] GetTakenFramePositions()
        {
            return BoxFrames.Keys.ToArray();
        }

        public int GetFrameCount()
        {
            return BoxFrames.Count;
        }

        public void OnKilled()
        {
            foreach (string frame in BoxFrames.Keys.ToList())
            {
                RemoveFrame(frame, true);
            }

            _ins._boxes.Remove(Box.net.ID);
            _ins._storedData.FrameData.Remove(Box.net.ID.Value);
        }
    }
    #endregion

    #region Admin Behavior
    //Players with Admin or Developer flag can always edit signs. We use this to open box for them.
    public class AdminBoxBehavior : FacepunchBehaviour
    {
        private BasePlayer Player { get; set; }
        private bool IsDown { get; set; }

        private void Awake()
        {
            Player = GetComponent<BasePlayer>();
        }

        private void Update()
        {
            if (Player.inventory.loot.IsLooting())
            {
                return;
            }

            bool isDown = Player.serverInput.IsDown(BUTTON.USE);
            if (isDown == IsDown)
            {
                return;
            }

            IsDown = isDown;
            if (!IsDown)
            {
                return;
            }

            PhotoFrame frame = _ins.Raycast<PhotoFrame>(Player);
            if (frame == null || !frame.IsLocked())
            {
                return;
            }

            FrameData frameData = _ins._frames[frame.net.ID];
            if (frameData == null)
            {
                return;
            }

            StartCoroutine(OpenBox(frame, frameData.BoxData.Box as IItemContainerEntity));
        }

        //Needed because we need to be sure the sign is destroyed on the client before we try to open the box
        private IEnumerator OpenBox(PhotoFrame frame, IItemContainerEntity box)
        {
            frame.DestroyOnClient(Player.Connection);
            yield return null;
            yield return null;

            box?.PlayerOpenLoot(Player);

            yield return new WaitForSeconds(0.05f);

            frame.SendNetworkUpdate();
        }

        public void DoDestroy()
        {
            Destroy(this);
        }
    }
    #endregion

    #region Classes
    public class PluginConfig
    {
        [DefaultValue("fb")]
        [JsonProperty(PropertyName = "Chat Command")]
        public string ChatCommand { get; set; }

        [DefaultValue(true)]
        [JsonProperty(PropertyName = "Refund on remove")]
        public bool RefundOnRemove { get; set; }

        [DefaultValue(1f)]
        [JsonProperty(PropertyName = "Refund % (0 none - 1 max)")]
        public float RefundPercentage { get; set; }

        [DefaultValue(5f)]
        [JsonProperty(PropertyName = "Max distance (Meters)")]
        public float MaxDistance { get; set; }

        [JsonProperty(PropertyName = "Cost")]
        public Hash<string, int> Cost { get; set; }

        [JsonProperty(PropertyName = "Sign Positions")]
        public Hash<string, ContainerData> SignPositions { get; set; }
    }

    public class StoredData
    {
        public Hash<ulong, Hash<string, ulong>> FrameData = new();
    }

    public class ContainerData
    {
        [JsonProperty(PropertyName = "Frame Item Shortname")]
        public string ItemShortname { get; set; }

        [JsonProperty(PropertyName = "Frame Positions")]
        public Hash<string, PositionData> PositionData { get; set; }
    }

    public class PositionData
    {
        [JsonConverter(typeof(Vector3Converter))]
        [JsonProperty(PropertyName = "Frame Position Offset")]
        public Vector3 Offset { get; set; }

        [JsonConverter(typeof(Vector3Converter))]
        [JsonProperty(PropertyName = "Frame Position Rotation")]
        public Vector3 Rotation { get; set; }

        [JsonProperty(PropertyName = "Include Position In All Option")]
        public bool IncludeAll { get; set; }

        [JsonIgnore]
        public string DisplayName { get; set; }
    }

    public class LangKeys
    {
        public const string Chat = nameof(Chat);
        public const string NoPermission = nameof(NoPermission);
        public const string HelpText = nameof(HelpText) + "_V1";
        public const string MissingItem = nameof(MissingItem);
        public const string MissingMessage = nameof(MissingMessage);
        public const string CostPre = nameof(CostPre) + "_V1";
        public const string Cost = nameof(Cost);
        public const string AddInvalidSyntax = nameof(AddInvalidSyntax) + "_V1";
        public const string InvalidAddFramePosition = nameof(InvalidAddFramePosition) + "_V1";
        public const string NotLookingAt = nameof(NotLookingAt);
        public const string AddSlotIsTaken = nameof(AddSlotIsTaken);
        public const string AddSuccess = nameof(AddSuccess);
        public const string RemoveSuccess = nameof(RemoveSuccess);
        public const string UnlockBuildingBlocked = nameof(UnlockBuildingBlocked);
        public const string UnlockSuccess = nameof(UnlockSuccess);
        public const string NotLookingAtFrame = nameof(NotLookingAtFrame);
        public const string NotLookingAtFrameBoxFrame = nameof(NotLookingAtFrameBoxFrame);
        public const string NotLookingAtFrameBox = nameof(NotLookingAtFrameBox);
        public const string NoFrameInPosition = nameof(NoFrameInPosition);
        public const string ContainerNotAllowed = nameof(ContainerNotAllowed) + "_V1";
        public const string NoUnlockPermission = nameof(NoUnlockPermission);
        public const string AddSubCommand = nameof(AddSubCommand);
        public const string RemoveSubCommand = nameof(RemoveSubCommand);
        public const string CostSubCommand = nameof(CostSubCommand);
        public const string UnlockSubCommand = nameof(UnlockSubCommand);
        public const string AllSubOption = nameof(AllSubOption);
    }
    #endregion

    #region JSON Converters
    public class Vector3Converter : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            Vector3 pos = (Vector3) value;
            writer.WriteValue($"{pos.x} {pos.y} {pos.z}");
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
            JsonSerializer serializer)
        {
            string[] values = reader.Value.ToString().Trim().Split(' ');
            return new Vector3(Convert.ToSingle(values[0]), Convert.ToSingle(values[1]), Convert.ToSingle(values[2]));
        }

        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(Vector3);
        }
    }
    #endregion
}