﻿using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Oxide.Core.Libraries.Covalence;
using System;
using System.Collections.Generic;
using System.Linq;
using Oxide.Core;
using Rust.Modular;

namespace Oxide.Plugins
{
    [Info("Car Spawn Settings", "WhiteThunder", "2.3.1")]
    [Description("Allows modular cars to spawn with configurable modules, health, fuel, and engine parts.")]
    internal class CarSpawnSettings : CovalencePlugin
    {
        #region Fields

        private readonly object False = false;
        private Configuration _config;
        private VanillaPresetCache _vanillaPresetCache = new();

        #endregion

        #region Hooks

        private void Init()
        {
            // Make sure presets are ready as soon as possible
            // Cars can spawn while generating a new map before OnServerInitialized()
            _config.Init(this);
        }

        private object OnVehicleModulesAssign(ModularCar car)
        {
            var vanillaPresets = car.spawnSettings.configurationOptions;
            if (vanillaPresets.Length == 1)
            {
                // Ignore the car if there's only 1 preset because it's probably a spawnable preset.
                return null;
            }

            var presetConfiguration = _config.ModulePresetMap.GetPresetConfigurationForSockets(car.TotalSockets);
            var numCustomPresets = presetConfiguration?.CustomPresets.Length ?? 0;

            var numTotalPresets = numCustomPresets;
            if (presetConfiguration is { UseVanillaPresets: true })
            {
                numTotalPresets += vanillaPresets.Length;
            }

            if (numTotalPresets > 0)
            {
                IList<IModuleDefinition> moduleDefinitions;

                var randomPresetIndex = UnityEngine.Random.Range(0, numTotalPresets);
                if (randomPresetIndex < numCustomPresets)
                {
                    moduleDefinitions = presetConfiguration.CustomPresets[randomPresetIndex];
                }
                else
                {
                    moduleDefinitions = _vanillaPresetCache.GetModulePreset(
                        vanillaPresets[randomPresetIndex - numCustomPresets]
                    );
                }

                AddCarModules(car, moduleDefinitions);
            }

            NextTick(() =>
            {
                if (car == null || car.IsDestroyed)
                    return;

                ProcessCar(car);
            });

            return False;
        }

        #endregion

        #region Commands

        [Command("carspawnsettings.fillcars")]
        private void CommandFillCars(IPlayer player)
        {
            if (!player.IsAdmin)
                return;

            var carsProcessed = 0;

            foreach (var entity in BaseNetworkable.serverEntities)
            {
                var car = entity as ModularCar;
                if (car == null || car.IsDestroyed)
                    continue;

                ProcessCar(car);
                carsProcessed++;
            }

            player.Reply(GetMessage(player.Id, Lang.FillSuccess, carsProcessed));
        }

        #endregion

        #region Helper Methods

        private bool BootstrapWasBlocked(ModularCar car)
        {
            return Interface.CallHook("CanBootstrapSpawnedCar", car) is false;
        }

        private void ProcessCar(ModularCar car)
        {
            if (car.OwnerID != 0 || BootstrapWasBlocked(car))
                return;

            BootstrapAfterModules(car);
        }

        private void AddCarModules(ModularCar car, IList<IModuleDefinition> modulePreset)
        {
            for (var i = 0; i < car.TotalSockets && i < modulePreset.Count; i++)
            {
                var moduleDefinition = modulePreset[i];
                var existingItem = car.Inventory.ModuleContainer.GetSlot(i);
                if (existingItem != null)
                    continue;

                var moduleItem = moduleDefinition.Create();
                if (moduleItem == null)
                    continue;

                moduleItem.conditionNormalized = _config.RandomizeModuleCondition();

                if (!car.TryAddModule(moduleItem, i))
                {
                    moduleItem.Remove();
                    break;
                }

                // Skip ahead if the current module takes multiple sockets.
                i += moduleDefinition.NumSockets - 1;
            }
        }

        private void BootstrapAfterModules(ModularCar car)
        {
            MaybeAddFuel(car);
            MaybeAddEngineParts(car);
        }

        private void MaybeAddFuel(ModularCar car)
        {
            var fuelAmount = _config.RandomizeFuelAmount();
            if (fuelAmount == 0)
                return;

            if (car.GetFuelSystem() is not EntityFuelSystem fuelSystem)
                return;

            var fuelContainer = fuelSystem.GetFuelContainer();
            if (fuelAmount < 0)
            {
                fuelAmount = fuelContainer.allowedItem.stackable;
            }

            var fuelItem = fuelContainer.inventory.FindItemByItemID(fuelContainer.allowedItem.itemid);
            if (fuelItem == null)
            {
                fuelContainer.inventory.AddItem(fuelContainer.allowedItem, fuelAmount);
            }
        }

        private void MaybeAddEngineParts(ModularCar car)
        {
            if (!_config.CanHaveEngineParts())
                return;

            foreach (var child in car.children)
            {
                var engineModule = child as VehicleModuleEngine;
                if (engineModule == null)
                    continue;

                var engineStorage = engineModule.GetContainer() as EngineStorage;
                if (engineStorage == null || !engineStorage.inventory.IsEmpty())
                    continue;

                AddPartsToEngineStorage(engineStorage);
                engineModule.RefreshPerformanceStats(engineStorage);
            }
        }

        private void AddPartsToEngineStorage(EngineStorage engineStorage)
        {
            if (engineStorage.inventory == null)
                return;

            var inventory = engineStorage.inventory;
            for (var i = 0; i < inventory.capacity; i++)
            {
                // Do nothing if there is an existing engine part
                var item = inventory.GetSlot(i);
                if (item != null)
                    continue;

                var tier = _config.RandomizeEnginePartTier();
                if (tier > 0)
                {
                    TryAddEngineItem(engineStorage, i, tier);
                }
            }
        }

        private bool TryAddEngineItem(EngineStorage engineStorage, int slot, int tier)
        {
            if (!engineStorage.allEngineItems.TryGetItem(tier, engineStorage.slotTypes[slot], out var output))
                return false;

            var component = output.GetComponent<ItemDefinition>();
            var item = ItemManager.Create(component);
            if (item == null)
                return false;

            item.conditionNormalized = _config.RandomizePartCondition();
            item.MoveToContainer(engineStorage.inventory, slot, allowStack: false);
            return true;
        }

        #endregion

        #region Module Definitions

        private interface IModuleDefinition
        {
            Item Create();
            int NumSockets { get; }
        }

        private class VanillaModuleDefinition : IModuleDefinition
        {
            public int NumSockets { get; }

            private ItemDefinition _itemDefinition;

            public VanillaModuleDefinition(ItemModVehicleModule socketItemDefinition)
            {
                NumSockets = socketItemDefinition.SocketsTaken;
                _itemDefinition = socketItemDefinition.GetComponent<ItemDefinition>();
            }

            public Item Create()
            {
                if ((object)_itemDefinition == null)
                    return null;

                return ItemManager.Create(_itemDefinition);
            }
        }

        private class VanillaPresetCache
        {
            private Dictionary<ModularCarPresetConfig, IList<IModuleDefinition>> _cache = new();

            public IList<IModuleDefinition> GetModulePreset(ModularCarPresetConfig presetConfig)
            {
                if (!_cache.TryGetValue(presetConfig, out var modules))
                {
                    var moduleDefinitionList = new List<IModuleDefinition>();

                    foreach (var socketItemDefinition in presetConfig.socketItemDefs)
                    {
                        if (socketItemDefinition == null)
                            continue;

                        moduleDefinitionList.Add(new VanillaModuleDefinition(socketItemDefinition));
                    }

                    modules = moduleDefinitionList.ToArray();
                    _cache[presetConfig] = modules;
                }

                return modules;
            }
        }

        #endregion

        #region Configuration

        private Configuration GetDefaultConfig() => new();

        private class Configuration : SerializableConfiguration
        {
            [JsonProperty("EnginePartsTier")]
            private int DeprecatedEnginePartsTier
            {
                set
                {
                    if (value == 1)
                    {
                        EngineParts.Tier1Chance = 100;
                    }
                    else if (value == 2)
                    {
                        EngineParts.Tier2Chance = 100;
                    }
                    else if (value == 3)
                    {
                        EngineParts.Tier3Chance = 100;
                    }
                }
            }

            [JsonProperty("FuelAmount", DefaultValueHandling = DefaultValueHandling.Ignore)]
            private int DeprecatedFuelAmount = 0;

            [JsonProperty("Engine parts")]
            public EnginePartConfiguration EngineParts = new();
            [JsonProperty("EngineParts")]
            public EnginePartConfiguration DeprecatedEngineParts { set => EngineParts = value; }

            [JsonProperty("Min fuel amount")]
            public int MinFuelAmount = 0;
            [JsonProperty("MinFuelAmount")]
            public int DeprecatedMinFuelAmount { set => MinFuelAmount = value; }

            [JsonProperty("Max fuel amount")]
            public int MaxFuelAmount = 0;
            [JsonProperty("MaxFuelAmount")]
            private int DeprecatedMaxFuelAmount { set => MaxFuelAmount = value; }

            [JsonProperty("Min health percent")]
            public float MinHealthPercent = 15.0f;
            [JsonProperty("MinHealthPercent")]
            private float DeprecatedMinHealthPercent { set => MinHealthPercent = value; }

            [JsonProperty("Max health percent")]
            public float MaxHealthPercent = 50.0f;
            [JsonProperty("MaxHealthPercent")]
            private float DeprecatedMaxHealthPercent { set => MaxHealthPercent = value; }
            [JsonProperty("HealthPercentage")]
            private float DeprecatedHealthPercentage
            {
                set
                {
                    MinHealthPercent = value;
                    MaxHealthPercent = value;
                }
            }

            [JsonProperty("Module presets")]
            public ModulePresetMap ModulePresetMap = new();
            [JsonProperty("ModulePresets")]
            private ModulePresetMap DeprecatedModulePresetMap { set => ModulePresetMap = value; }

            public void Init(CarSpawnSettings plugin)
            {
                ModulePresetMap.Init(plugin);
            }

            public float RandomizeModuleCondition()
            {
                return RandomizeCondition(MinHealthPercent, MaxHealthPercent);
            }

            public int RandomizeFuelAmount()
            {
                if (DeprecatedFuelAmount != 0)
                    return DeprecatedFuelAmount;

                if (MinFuelAmount == 0 && MaxFuelAmount == 0)
                    return 0;

                if (MinFuelAmount == MaxFuelAmount)
                    return MinFuelAmount;

                return UnityEngine.Random.Range(MinFuelAmount, MaxFuelAmount + 1);
            }

            public bool CanHaveEngineParts()
            {
                return EngineParts.Tier1Chance > 0
                       || EngineParts.Tier2Chance > 0
                       || EngineParts.Tier3Chance > 0;
            }

            public int RandomizeEnginePartTier()
            {
                if (EngineParts.Tier3Chance > 0
                    && (EngineParts.Tier3Chance >= 100 || UnityEngine.Random.Range(0, 100) < EngineParts.Tier3Chance))
                    return 3;

                if (EngineParts.Tier2Chance > 0
                    && (EngineParts.Tier2Chance >= 100 || UnityEngine.Random.Range(0, 100) < EngineParts.Tier2Chance))
                    return 2;

                if (EngineParts.Tier1Chance > 0
                    && (EngineParts.Tier1Chance >= 100 || UnityEngine.Random.Range(0, 100) < EngineParts.Tier1Chance))
                    return 1;

                return 0;
            }

            public float RandomizePartCondition()
            {
                return RandomizeCondition(
                    EngineParts.MinConditionPercent,
                    EngineParts.MaxConditionPercent
                );
            }

            private float RandomizeCondition(float minPercent, float maxPercent)
            {
                if (minPercent >= 100)
                    return 1;

                return UnityEngine.Mathf.Round(
                    UnityEngine.Random.Range(minPercent, Math.Max(minPercent, maxPercent))
                ) / 100f;
            }
        }

        [JsonObject(MemberSerialization.OptIn)]
        private class EnginePartConfiguration
        {
            [JsonProperty("Tier 1 chance")]
            public int Tier1Chance = 0;
            [JsonProperty("Tier1Chance")]
            private int DeprecatedTier1Chance { set => Tier1Chance = value; }

            [JsonProperty("Tier 2 chance")]
            public int Tier2Chance = 0;
            [JsonProperty("Tier2Chance")]
            private int DeprecatedTier2Chance { set => Tier2Chance = value; }

            [JsonProperty("Tier 3 chance")]
            public int Tier3Chance = 0;
            [JsonProperty("Tier3Chance")]
            private int DeprecatedTier3Chance { set => Tier3Chance = value; }

            [JsonProperty("Min condition percent")]
            public float MinConditionPercent = 100f;
            [JsonProperty("MinConditionPercent")]
            private float DeprecatedMinConditionPercent { set => MinConditionPercent = value; }

            [JsonProperty("Max condition percent")]
            public float MaxConditionPercent = 100f;
            [JsonProperty("MaxConditionPercent")]
            private float DeprecatedMaxConditionPercent { set => MaxConditionPercent = value; }
        }

        [JsonObject(MemberSerialization.OptIn)]
        private class ModulePresetMap
        {
            [JsonProperty("2 sockets")]
            public ModulePresetConfiguration PresetsFor2Sockets = new();
            [JsonProperty("2Sockets")]
            private ModulePresetConfiguration DeprecatedPresetsFor2Sockets { set => PresetsFor2Sockets = value; }

            [JsonProperty("3 sockets")]
            public ModulePresetConfiguration PresetsFor3Sockets = new();
            [JsonProperty("3Sockets")]
            private ModulePresetConfiguration DeprecatedPresetsFor3Sockets { set => PresetsFor3Sockets = value; }

            [JsonProperty("4 sockets")]
            public ModulePresetConfiguration PresetsFor4Sockets = new();
            [JsonProperty("4Sockets")]
            private ModulePresetConfiguration DeprecatedPresetsFor4Sockets { set => PresetsFor4Sockets = value; }

            public void Init(CarSpawnSettings plugin)
            {
                PresetsFor2Sockets.Init(plugin);
                PresetsFor3Sockets.Init(plugin);
                PresetsFor4Sockets.Init(plugin);
            }

            public ModulePresetConfiguration GetPresetConfigurationForSockets(int totalSockets)
            {
                if (totalSockets == 4)
                    return PresetsFor4Sockets;

                if (totalSockets == 3)
                    return PresetsFor3Sockets;

                if (totalSockets == 2)
                    return PresetsFor2Sockets;

                return null;
            }
        }

        [JsonObject(MemberSerialization.OptIn)]
        private class ModuleDefinition : IModuleDefinition
        {
            [JsonProperty("Item short name")]
            public string ItemShortName;

            [JsonProperty("Item skin ID")]
            public ulong SkinId;

            [JsonIgnore]
            public int NumSockets { get; private set; }= 1;

            [JsonIgnore]
            private ItemDefinition _itemDefinition;

            public void Init(CarSpawnSettings plugin = null)
            {
                // Null/empty short name indicates a blank socket between modules.
                if (string.IsNullOrEmpty(ItemShortName))
                    return;

                _itemDefinition = ItemManager.FindItemDefinition(ItemShortName);
                if (_itemDefinition == null)
                {
                    plugin?.LogError($"Unrecognized module item short name: {ItemShortName}");
                    return;
                }

                var vehicleMod = _itemDefinition.GetComponent<ItemModVehicleModule>();
                if (vehicleMod == null)
                {
                    plugin?.LogError("No vehicle module found for item: {0}", ItemShortName);
                    _itemDefinition = null;
                    return;
                }

                NumSockets = vehicleMod.SocketsTaken;
            }

            public Item Create()
            {
                if ((object)_itemDefinition == null)
                    return null;

                return ItemManager.Create(_itemDefinition, 1, SkinId);
            }
        }

        private class ModulePresetConfiguration
        {
            [JsonProperty("Use vanilla presets")]
            public bool UseVanillaPresets = true;
            [JsonProperty("UseVanillaPresets")]
            private bool DeprecatedUseVanillaPresets { set => UseVanillaPresets = value; }

            [JsonProperty("Custom presets")]
            public ModuleDefinition[][] CustomPresets = Array.Empty<ModuleDefinition[]>();
            [JsonProperty("CustomPresets")]
            private object[][] DeprecatedCustomPresets { set => CustomPresets = ParseLegacyPresets(value); }

            public void Init(CarSpawnSettings plugin)
            {
                foreach (var presetList in CustomPresets)
                {
                    foreach (var moduleDefinition in presetList)
                    {
                        moduleDefinition.Init(plugin);
                    }
                }
            }

            private ModuleDefinition[][] ParseLegacyPresets(object[][] legacyPresetList)
            {
                var presetList = new List<ModuleDefinition[]>();

                foreach (var moduleIdentifierList in legacyPresetList)
                {
                    var modulePreset = new List<ModuleDefinition>();
                    foreach (var moduleIdentifier in moduleIdentifierList)
                    {
                        modulePreset.Add(ParseLegacyModuleDefinition(moduleIdentifier));
                    }
                    presetList.Add(modulePreset.ToArray());
                }

                return presetList.ToArray();
            }

            private ModuleDefinition ParseLegacyModuleDefinition(object moduleIdentifier)
            {
                if (moduleIdentifier is int or long)
                {
                    var moduleId = moduleIdentifier is long identifier
                        ? Convert.ToInt32(identifier)
                        : (int)moduleIdentifier;

                    if (moduleId == 0)
                        return new ModuleDefinition();

                    var itemDefinition = ItemManager.FindItemDefinition(moduleId);
                    if (itemDefinition == null)
                        return new ModuleDefinition();

                    return new ModuleDefinition
                    {
                        ItemShortName = itemDefinition.shortname,
                    };
                }

                if (moduleIdentifier is string moduleString)
                {
                    if (int.TryParse(moduleString, out var parsedItemId))
                    {
                        if (parsedItemId == 0)
                            return new ModuleDefinition();

                        var itemDefinition = ItemManager.FindItemDefinition(parsedItemId);
                        if (itemDefinition == null)
                            return new ModuleDefinition();
                    }

                    return new ModuleDefinition
                    {
                        ItemShortName = moduleString,
                    };
                }

                return new ModuleDefinition();
            }
        }

        #region Configuration Helpers

        private class SerializableConfiguration
        {
            public string ToJson() => JsonConvert.SerializeObject(this);

            public Dictionary<string, object> ToDictionary() => JsonHelper.Deserialize(ToJson()) as Dictionary<string, object>;
        }

        private static class JsonHelper
        {
            public static object Deserialize(string json) => ToObject(JToken.Parse(json));

            private static object ToObject(JToken token)
            {
                switch (token.Type)
                {
                    case JTokenType.Object:
                        return token.Children<JProperty>()
                                    .ToDictionary(prop => prop.Name,
                                                  prop => ToObject(prop.Value));

                    case JTokenType.Array:
                        return token.Select(ToObject).ToList();

                    default:
                        return ((JValue)token).Value;
                }
            }
        }

        private bool MaybeUpdateConfig(SerializableConfiguration config)
        {
            var currentWithDefaults = config.ToDictionary();
            var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value);
            return MaybeUpdateConfigDict(currentWithDefaults, currentRaw);
        }

        private bool MaybeUpdateConfigDict(Dictionary<string, object> currentWithDefaults, Dictionary<string, object> currentRaw)
        {
            var changed = false;

            foreach (var key in currentWithDefaults.Keys)
            {
                if (currentRaw.TryGetValue(key, out var currentRawValue))
                {
                    var currentDictValue = currentRawValue as Dictionary<string, object>;
                    if (currentWithDefaults[key] is Dictionary<string, object> defaultDictValue)
                    {
                        if (currentDictValue == null)
                        {
                            currentRaw[key] = currentWithDefaults[key];
                            changed = true;
                        }
                        else if (MaybeUpdateConfigDict(defaultDictValue, currentDictValue))
                            changed = true;
                    }
                }
                else
                {
                    currentRaw[key] = currentWithDefaults[key];
                    changed = true;
                }
            }

            return changed;
        }

        protected override void LoadDefaultConfig() => _config = GetDefaultConfig();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<Configuration>();
                if (_config == null)
                {
                    throw new JsonException();
                }

                if (MaybeUpdateConfig(_config))
                {
                    LogWarning("Configuration appears to be outdated; updating and saving");
                    SaveConfig();
                }
            }
            catch (Exception e)
            {
                LogError(e.Message);
                LogWarning($"Configuration file {Name}.json is invalid; using defaults");
                LoadDefaultConfig();
            }
        }

        protected override void SaveConfig()
        {
            Log($"Configuration changes saved to {Name}.json");
            Config.WriteObject(_config, true);
        }

        #endregion

        #endregion

        #region Localization

        private string GetMessage(string userId, string messageName, params object[] args)
        {
            var message = lang.GetMessage(messageName, this, userId);
            return args.Length > 0 ? string.Format(message, args) : message;
        }

        private static class Lang
        {
            public const string FillSuccess = "Fill.Success";
        }

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                [Lang.FillSuccess] = "Processed {0} cars.",
            }, this, "en");
        }

        #endregion
    }
}
