using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
using Random = UnityEngine.Random;

namespace Oxide.Plugins
{
    [Info("Quick Smelt", "misticos + WhiteThunder", "5.1.14")]
    [Description("Increases the speed of the furnace smelting")]
    class QuickSmelt : RustPlugin
    {
        #region Variables

        private static QuickSmelt _instance;

        private const string PermissionUse = "quicksmelt.use";

        private readonly object False = false;

        #endregion

        #region Configuration

        private static Configuration _config;

        private class Configuration
        {
            [JsonProperty(PropertyName = "Use Permission")]
            public bool UsePermission = true;

            [JsonProperty(PropertyName = "Speed Multipliers", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public Dictionary<string, float> SpeedMultipliers = new Dictionary<string, float>
            {
                { "global", 1.0f },
                { "furnace.shortname", 1.0f }
            };

            [JsonProperty(PropertyName = "Fuel Usage Speed Multipliers",
                ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public Dictionary<string, float> FuelSpeedMultipliers = new Dictionary<string, float>
            {
                { "global", 1.0f },
                { "furnace.shortname", 1.0f }
            };

            [JsonProperty(PropertyName = "Fuel Usage Multipliers",
                ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public Dictionary<string, int> FuelUsageMultipliers = new Dictionary<string, int>
            {
                { "global", 1 },
                { "furnace.shortname", 1 }
            };

            [JsonProperty(PropertyName = "Output Multipliers", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public Dictionary<string, Dictionary<string, float>> OutputMultipliers =
                new Dictionary<string, Dictionary<string, float>>
                {
                    {
                        "global", new Dictionary<string, float>
                        {
                            { "global", 1.0f }
                        }
                    },
                    {
                        "furnace.shortname", new Dictionary<string, float>
                        {
                            { "item.shortname", 1.0f }
                        }
                    }
                };

            [JsonProperty(PropertyName = "Whitelist", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public Dictionary<string, List<string>> Whitelist = new Dictionary<string, List<string>>
            {
                {
                    "global", new List<string>
                    {
                        "item.shortname"
                    }
                },
                {
                    "furnace.shortname", new List<string>
                    {
                        "item.shortname"
                    }
                }
            };

            [JsonProperty(PropertyName = "Blacklist", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public Dictionary<string, List<string>> Blacklist = new Dictionary<string, List<string>>
            {
                {
                    "global", new List<string>
                    {
                        "item.shortname"
                    }
                },
                {
                    "furnace.shortname", new List<string>
                    {
                        "item.shortname"
                    }
                }
            };

            [JsonProperty(PropertyName = "Smelting Frequencies (Smelt items every N smelting ticks)",
                ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public Dictionary<string, int> SmeltingFrequencies = new Dictionary<string, int>
            {
                { "global", 1 },
                { "furnace.shortname", 1 }
            };

            [JsonProperty(PropertyName = "Debug")]
            public bool Debug = false;

            public void OnServerInitialized(QuickSmelt plugin)
            {
                foreach (var entry in OutputMultipliers.Values)
                {
                    ValidateItemNames(plugin, entry.Keys);
                }

                foreach (var entry in Whitelist.Values)
                {
                    ValidateItemNames(plugin, entry);
                }

                foreach (var entry in Blacklist.Values)
                {
                    ValidateItemNames(plugin, entry);
                }

                var validFurnaceShortNames = GetValidFurnaceShortNames();
                var validDeployables = GetItemShortNameToDeployableShortName<BaseOven>();
                ValidateFurnaceShortNames(plugin, validFurnaceShortNames, validDeployables, SpeedMultipliers.Keys);
                ValidateFurnaceShortNames(plugin, validFurnaceShortNames, validDeployables, FuelSpeedMultipliers.Keys);
                ValidateFurnaceShortNames(plugin, validFurnaceShortNames, validDeployables, FuelUsageMultipliers.Keys);
                ValidateFurnaceShortNames(plugin, validFurnaceShortNames, validDeployables, OutputMultipliers.Keys);
                ValidateFurnaceShortNames(plugin, validFurnaceShortNames, validDeployables, Whitelist.Keys);
                ValidateFurnaceShortNames(plugin, validFurnaceShortNames, validDeployables, Blacklist.Keys);
            }

            private void ValidateItemNames(QuickSmelt plugin, IEnumerable<string> itemShortNameList)
            {
                foreach (var itemShortName in itemShortNameList)
                {
                    if (itemShortName != "global"
                        && itemShortName != "item.shortname"
                        && ItemManager.FindItemDefinition(itemShortName) == null)
                    {
                        plugin.PrintWarning($"[Configuration] {itemShortName} is not a valid item.");
                    }
                }
            }

            private void ValidateFurnaceShortNames(QuickSmelt plugin, HashSet<string> validFurnaceShortNames, Dictionary<string, string> validFurnaces, IEnumerable<string> shortNameList)
            {
                foreach (var shortName in shortNameList)
                {
                    if (shortName == "global" || shortName == "furnace.shortname" || validFurnaceShortNames.Contains(shortName))
                        continue;

                    var message = $"[Configuration] {shortName} is not a valid furnace short name.";

                    // People often put item short names into the config where an entity short name is expected.
                    // This will suggest the correct name to the user: electric.furnace -> electricfurnace.deployed
                    string furnaceShortName;
                    if (validFurnaces.TryGetValue(shortName, out furnaceShortName))
                    {
                        message += $" Did you mean {furnaceShortName}?";
                    }

                    plugin.PrintWarning(message);
                }
            }

            private static HashSet<string> GetValidFurnaceShortNames()
            {
                var validShortNames = new HashSet<string>();

                foreach (var prefabPath in GameManifest.Current.entities)
                {
                    var furnace = GameManager.server.FindPrefab(prefabPath)?.GetComponent<BaseOven>();
                    if (furnace == null)
                        continue;

                    validShortNames.Add(furnace.ShortPrefabName);
                }

                return validShortNames;
            }

            private static Dictionary<string, string> GetItemShortNameToDeployableShortName<T>() where T : BaseEntity
            {
                var itemsToDeployables = new Dictionary<string, string>();

                foreach (var itemDefinition in ItemManager.itemList)
                {
                    var itemModDeployable = itemDefinition.GetComponent<ItemModDeployable>();
                    if (itemModDeployable == null)
                        continue;

                    var entity = itemModDeployable.entityPrefab.GetEntity() as T;
                    if (entity == null)
                        continue;

                    itemsToDeployables[itemDefinition.shortname] = entity.ShortPrefabName;
                }

                return itemsToDeployables;
            }
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<Configuration>();
                if (_config == null) throw new Exception();
            }
            catch
            {
                PrintError("Your configuration file contains an error. Using default configuration values.");
                LoadDefaultConfig();
            }
        }

        protected override void LoadDefaultConfig() => _config = new Configuration();

        protected override void SaveConfig() => Config.WriteObject(_config);

        #endregion

        #region Hooks

        private void Unload()
        {
            var ovens = BaseNetworkable.serverEntities.OfType<BaseOven>().ToArray();
            PrintDebug($"Processing BaseOven(s).. Amount: {ovens.Length}.");

            for (var i = 0; i < ovens.Length; i++)
            {
                var oven = ovens[i];
                var component = oven.GetComponent<FurnaceController>();

                if (oven.IsOn())
                {
                    PrintDebug("Oven is on. Restarted cooking");
                    component.StopCooking();
                    oven.StartCooking();
                }

                UnityEngine.Object.Destroy(component);
            }

            PrintDebug("Done.");
        }

        private void OnServerInitialized()
        {
            _instance = this;
            _config.OnServerInitialized(this);
            permission.RegisterPermission(PermissionUse, this);

            var ovens = BaseNetworkable.serverEntities.OfType<BaseOven>().ToArray();
            PrintDebug($"Processing BaseOven(s).. Amount: {ovens.Length}.");

            for (var i = 0; i < ovens.Length; i++)
            {
                var oven = ovens[i];

                OnEntitySpawned(oven);
            }

            timer.Once(1f, () =>
            {
                for (var i = 0; i < ovens.Length; i++)
                {
                    var oven = ovens[i];
                    if (oven == null)
                    {
                        continue;
                    }

                    var component = oven.gameObject.GetComponent<FurnaceController>();

                    if (oven.IsDestroyed || !oven.IsOn() || !CanUse(oven.OwnerID))
                        continue;

                    component.StartCooking();
                }
            });
        }

        private void OnEntitySpawned(BaseNetworkable entity)
        {
            var oven = entity as BaseOven;
            if (oven == null)
                return;

            oven.gameObject.AddComponent<FurnaceController>();
        }

        // Electric furnaces can be started via electricity.
        // Normal furnaces can be started via igniter.
        private object OnOvenStart(StorageContainer oven)
        {
            if (oven is BaseFuelLightSource)
                return null;

            if (!CanUse(oven.OwnerID))
                return null;

            oven.gameObject.GetComponent<FurnaceController>().StartCooking();
            return False;
        }

        private object OnOvenToggle(StorageContainer oven, BasePlayer player)
        {
            if (oven is BaseFuelLightSource || oven.needsBuildingPrivilegeToUse && !player.CanBuild())
                return null;

            PrintDebug("OnOvenToggle called");
            var component = oven.gameObject.GetComponent<FurnaceController>();
            var canUse = CanUse(oven.OwnerID) || CanUse(player.userID);
            if (oven.IsOn())
            {
                component.StopCooking();
            }
            else
            {
                if (canUse)
                {
                    component.StartCooking();
                }
                else
                {
                    PrintDebug($"No permission ({player.userID})");
                    return null;
                }
            }

            return False;
        }

        #endregion

        #region Helpers

        private bool CanUse(ulong id) =>
            !_config.UsePermission || permission.UserHasPermission(id.ToString(), PermissionUse);

        private static void PrintDebug(string message)
        {
            if (_config.Debug)
                Debug.Log($"DEBUG ({_instance.Name}) > " + message);
        }

        #endregion

        #region Controller

        public class FurnaceController : FacepunchBehaviour
        {
            private BaseOven _oven;

            private BaseOven Furnace
            {
                get
                {
                    if (_oven == null)
                        _oven = GetComponent<BaseOven>();

                    return _oven;
                }
            }

            private float _speedMultiplier;

            private float _fuelSpeedMultiplier;

            private int _fuelUsageMultiplier;

            private int _smeltingFrequency;

            private Dictionary<string, float> _outputModifiers;

            private List<Item> _itemsToCook = new List<Item>();

            private float OutputMultiplier(string shortname)
            {
                float modifier;
                if (_outputModifiers == null || !_outputModifiers.TryGetValue(shortname, out modifier) &&
                    !_outputModifiers.TryGetValue("global", out modifier))
                    modifier = 1.0f;

                PrintDebug($"{shortname} modifier: {modifier}");
                return modifier;
            }

            private List<string> _blacklist;
            private List<string> _whitelist;

            private bool? IsAllowed(string shortname)
            {
                if (_blacklist != null && _blacklist.Contains(shortname))
                    return false;

                if (_whitelist != null && _whitelist.Contains(shortname))
                    return true;

                return null;
            }

            private void Awake()
            {
                // Well, sorry for my complicated code. But that should work faster! :)

                float modifierF; // float modifier
                int modifierI; // int modifier

                if (!_config.SpeedMultipliers.TryGetValue(Furnace.ShortPrefabName, out modifierF) &&
                    !_config.SpeedMultipliers.TryGetValue("global", out modifierF))
                    modifierF = 1.0f;

                _speedMultiplier = modifierF;

                if (!_config.FuelSpeedMultipliers.TryGetValue(Furnace.ShortPrefabName, out modifierF) &&
                    !_config.FuelSpeedMultipliers.TryGetValue("global", out modifierF))
                    modifierF = 1.0f;

                _fuelSpeedMultiplier = modifierF;

                if (!_config.FuelUsageMultipliers.TryGetValue(Furnace.ShortPrefabName, out modifierI) &&
                    !_config.FuelUsageMultipliers.TryGetValue("global", out modifierI))
                    modifierI = 1;

                _fuelUsageMultiplier = modifierI;

                if (!_config.SmeltingFrequencies.TryGetValue(Furnace.ShortPrefabName, out modifierI) &&
                    !_config.SmeltingFrequencies.TryGetValue("global", out modifierI))
                    modifierI = 1;

                _smeltingFrequency = modifierI;

                if (!_config.OutputMultipliers.TryGetValue(Furnace.ShortPrefabName, out _outputModifiers) &&
                    !_config.OutputMultipliers.TryGetValue("global", out _outputModifiers))
                {
                    // ignored
                }

                if ((!_config.Blacklist.TryGetValue(Furnace.ShortPrefabName, out _blacklist) &&
                     !_config.Blacklist.TryGetValue("global", out _blacklist)) &
                    (!_config.Whitelist.TryGetValue(Furnace.ShortPrefabName, out _whitelist) &&
                     !_config.Whitelist.TryGetValue("global", out _whitelist)))
                {
                    // ignored
                }
            }

            private Item FindBurnable()
            {
                if (Furnace.inventory == null)
                    return null;

                var burnable = Interface.Call<Item>("OnFindBurnable", _oven);
                if (burnable != null)
                {
                    return burnable;
                }

                foreach (var item in Furnace.inventory.itemList)
                {
                    if (!_oven.IsBurnableItem(item))
                        continue;

                    return item;
                }

                return null;
            }

            public void Cook()
            {
                var itemBurnable = FindBurnable();
                if (Interface.CallHook("OnOvenCook", _oven, itemBurnable) != null)
                {
                    return;
                }

                if (itemBurnable == null && !Furnace.CanRunWithNoFuel)
                {
                    StopCooking();
                    return;
                }

                foreach (var itemCooking in _oven.inventory.itemList)
                {
                    if (itemCooking.position >= _oven._inputSlotIndex &&
                        itemCooking.position < _oven._inputSlotIndex + _oven.inputSlots &&
                        !itemCooking.HasFlag(global::Item.Flag.Cooking))
                    {
                        itemCooking.SetFlag(global::Item.Flag.Cooking, true);
                        itemCooking.MarkDirty();
                    }
                }

                SmeltItems();

                var slot = Furnace.GetSlot(BaseEntity.Slot.FireMod);
                if (slot)
                {
                    slot.SendMessage("Cook", 0.5f, SendMessageOptions.DontRequireReceiver);
                }

                if (itemBurnable != null)
                {
                    var burnable = itemBurnable.info.GetComponent<ItemModBurnable>();
                    itemBurnable.fuel -= 0.5f * (Furnace.cookingTemperature / 200f) * _fuelSpeedMultiplier;

                    if (!itemBurnable.HasFlag(global::Item.Flag.OnFire))
                    {
                        itemBurnable.SetFlag(global::Item.Flag.OnFire, true);
                        itemBurnable.MarkDirty();
                    }

                    if (itemBurnable.fuel <= 0f)
                    {
                        ConsumeFuel(itemBurnable, burnable);
                    }
                }

                Interface.CallHook("OnOvenCooked", _oven, itemBurnable, slot);
            }

            private void ConsumeFuel(Item fuel, ItemModBurnable burnable)
            {
                if (Interface.CallHook("OnFuelConsume", _oven, fuel, burnable) != null)
                {
                    return;
                }

                if (Furnace.allowByproductCreation && burnable.byproductItem != null &&
                    Random.Range(0f, 1f) > burnable.byproductChance)
                {
                    var def = burnable.byproductItem;
                    var item = ItemManager.Create(def,
                        (int)(burnable.byproductAmount * OutputMultiplier(def.shortname) * Mathf.Min(_speedMultiplier, fuel.amount))); // It's fuel multiplier

                    if (!item.MoveToContainer(Furnace.inventory))
                    {
                        StopCooking();
                        item.Drop(Furnace.inventory.dropPosition, Furnace.inventory.dropVelocity);
                    }
                }

                var fuelToConsume = (int)(_fuelUsageMultiplier * _speedMultiplier);

                if (fuel.amount <= fuelToConsume)
                {
                    fuel.Remove();
                    return;
                }

                fuel.UseItem(fuelToConsume);
                fuel.fuel = burnable.fuelAmount;
                fuel.MarkDirty();

                Interface.CallHook("OnFuelConsumed", _oven, fuel, burnable);
            }

            private void SmeltItems()
            {
                _itemsToCook.Clear();

                foreach (var item in Furnace.inventory.itemList)
                {
                    if (item.HasFlag(global::Item.Flag.Cooking))
                    {
                        _itemsToCook.Add(item);
                    }
                }

                foreach (var item in _itemsToCook)
                {
                    if (item == null || !item.IsValid())
                        continue;

                    var cookable = item.info.GetComponent<ItemModCookable>();
                    if (cookable == null)
                        continue;

                    var isAllowed = IsAllowed(item.info.shortname);
                    if (isAllowed != null &&
                        !isAllowed.Value) // Allowed is false? Okay, no problem. Don't cook this item
                        continue;

                    var temperature = item.temperature;
                    if (!cookable.CanBeCookedByAtTemperature(temperature) && isAllowed == null || item.cookTimeLeft < 0)
                    {
                        if (!cookable.setCookingFlag || !item.HasFlag(global::Item.Flag.Cooking))
                            continue;

                        item.SetFlag(global::Item.Flag.Cooking, false);
                        item.MarkDirty();

                        continue;
                    }

                    if (cookable.setCookingFlag && !item.HasFlag(global::Item.Flag.Cooking))
                    {
                        item.SetFlag(global::Item.Flag.Cooking, true);
                        item.MarkDirty();
                    }

                    item.cookTimeLeft -= 0.5f * _oven.GetSmeltingSpeed() / _itemsToCook.Count;
                    if (item.cookTimeLeft > 0)
                    {
                        item.MarkDirty();
                        continue;
                    }

                    var cookTimeInverted = item.cookTimeLeft * -1;
                    var amountConsumed = (int)((1 + Mathf.FloorToInt(cookTimeInverted / cookable.cookTime)) * _speedMultiplier);

                    item.cookTimeLeft = cookable.cookTime - cookTimeInverted % cookable.cookTime;
                    amountConsumed = Math.Min(amountConsumed, item.amount);

                    if (item.amount > amountConsumed)
                    {
                        item.amount -= amountConsumed;
                        item.MarkDirty();
                    }
                    else
                    {
                        item.Remove();
                    }

                    if (cookable.becomeOnCooked == null)
                        continue;

                    var itemProduced = ItemManager.Create(cookable.becomeOnCooked,
                        (int)(cookable.amountOfBecome * amountConsumed *
                              OutputMultiplier(cookable.becomeOnCooked.shortname)));

                    if (itemProduced == null || itemProduced.MoveToContainer(item.parent))
                        continue;

                    itemProduced.Drop(item.parent.dropPosition, item.parent.dropVelocity);

                    StopCooking();
                }

                _itemsToCook.Clear();
            }

            public void StartCooking()
            {
                if (!Furnace.CanRunWithNoFuel && FindBurnable() == null)
                {
                    PrintDebug("No burnable.");
                    return;
                }

                StopCooking();

                PrintDebug("Starting cooking..");
                Furnace.inventory.temperature = Furnace.cookingTemperature;
                Furnace.UpdateAttachmentTemperature();

                PrintDebug($"Speed Multiplier: {_speedMultiplier}");
                Furnace.InvokeRepeating(Cook, 0.5f, 0.5f);
                Furnace.SetFlag(BaseEntity.Flags.On, true);
            }

            public void StopCooking()
            {
                PrintDebug("Stopping cooking..");
                Furnace.CancelInvoke(Cook);
                Furnace.StopCooking();
            }
        }

        #endregion
    }
}