/***********************************************************************************************************************/
/*** DO NOT edit this file! Edit the files under `oxide/config` and/or `oxide/lang`, created once plugin has loaded. ***/
/*** Please note, support cannot be provided if the plugin has been modified. Please use a fresh copy if modified.   ***/
/***********************************************************************************************************************/

//#define DEBUG

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;

namespace Oxide.Plugins
{
    [Info("Heal", "Wulf", "3.1.1")]
    [Description("Allows players with permission to heal themselves or others")]
    public class Heal : CovalencePlugin
    {
        #region Configuration

        private Configuration config;

        private class Configuration
        {
            [JsonProperty("Command cooldown in seconds (0 to disable)")]
            public int CommandCooldown = 30;

            [JsonProperty("Maximum heal amount")]
            public int MaxHealAmount = 100;

            [JsonProperty("Notify target when healed")]
            public bool NotifyTarget = true;

            [JsonProperty("Use permission system")]
            public bool UsePermissions = true;

            public string ToJson() => JsonConvert.SerializeObject(this);

            public Dictionary<string, object> ToDictionary() => JsonConvert.DeserializeObject<Dictionary<string, object>>(ToJson());
        }

        protected override void LoadDefaultConfig() => config = new Configuration();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<Configuration>();
                if (config == null)
                {
                    throw new JsonException();
                }

                if (!config.ToDictionary().Keys.SequenceEqual(Config.ToDictionary(x => x.Key, x => x.Value).Keys))
                {
                    LogWarning("Configuration appears to be outdated; updating and saving");
                    SaveConfig();
                }
            }
            catch
            {
                LogWarning($"Configuration file {Name}.json is invalid; using defaults");
                LoadDefaultConfig();
            }
        }

        protected override void SaveConfig()
        {
            LogWarning($"Configuration changes saved to {Name}.json");
            Config.WriteObject(config, true);
        }

        #endregion Configuration

        #region Localization

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["CommandCooldown"] = "Wait a bit before attempting to use '{0}' again",
                ["CommandHeal"] = "heal",
                ["CommandHealAll"] = "healall",
                ["CommandHealPlayer"] = "healplayer",
                ["NotAllowed"] = "You are not allowed to use the '{0}' command",
                ["NoPlayersFound"] = "No players found with name or ID '{0}'",
                ["NoPlayersOnline"] = "There are no players online to heal",
                ["PlayerAtMaxHealth"] = "{0} is already at max health",
                ["PlayerNotFound"] = "Player '{0}' was not found",
                ["PlayerWasHealed"] = "{0} was healed {1}",
                ["PlayerWasNotHealed"] = "{0} was unable to be healed",
                ["PlayersFound"] = "Multiple players were found, please specify: {0}",
                ["PlayersOnly"] = "Command '{0}' can only be used by players",
                ["PlayersHealed"] = "All players have been healed {0}!",
                ["YouAreAtMaxHealth"] = "You are already at max health",
                ["YouWereHealed"] = "You were healed {0}",
                ["YouWereNotHealed"] = "You were unable to be healed",
                ["UsageHeal"] = "Usage: {0} [amount] -- Heal self by specfied or default amount",
                ["UsageHealAll"] = "Usage: {0} [amount] -- Heal all players by specfied or default amount",
                ["UsageHealPlayer"] = "Usage: {0} <player name or id> [amount] -- Heal player by specfied or default amount"
            }, this);
        }

        #endregion Localization

        #region Initialization

        private readonly Hash<string, float> cooldowns = new Hash<string, float>();

        private const string permSelf = "heal.self";
        private const string permAll = "heal.all";
        private const string permPlayer = "heal.player";
        private const string permNoCooldown = "heal.nocooldown";

        private void Init()
        {
            AddLocalizedCommand(nameof(CommandHeal));
            AddLocalizedCommand(nameof(CommandHealAll));
            AddLocalizedCommand(nameof(CommandHealPlayer));

            permission.RegisterPermission(permSelf, this);
            permission.RegisterPermission(permAll, this);
            permission.RegisterPermission(permPlayer, this);
            permission.RegisterPermission(permNoCooldown, this);
            MigratePermission("healer.self", permSelf);
            MigratePermission("healer.all", permAll);
            MigratePermission("healer.others", permPlayer);
        }

        #endregion Initialization

        private bool HasCooldown(IPlayer player, string command)
        {
            if (config.CommandCooldown > 0 && !player.IsServer && !player.HasPermission(permNoCooldown))
            {
                if (!cooldowns.ContainsKey(player.Id))
                {
                    cooldowns.Add(player.Id, 0f);
                }

                if (cooldowns[player.Id] + config.CommandCooldown > Interface.Oxide.Now)
                {
                    Message(player, "CommandCooldown", command);
                    return true;
                }
            }

            return false;
        }

        private object HealPlayer(IPlayer target, float amount)
        {
            float health = target.Health;
            float healthDiff = target.MaxHealth - health;
            amount = amount < healthDiff ? amount : healthDiff;
            if (target.Health < target.MaxHealth)
            {
                target.Heal(amount);
            }
#if RUST
            BasePlayer basePlayer = target.Object as BasePlayer; // TODO: Remove Rust-specifics if implemented in core
            basePlayer.metabolism.bleeding.value = 0;
            basePlayer.metabolism.calories.value += amount;
            basePlayer.metabolism.dirtyness.value = 0;
            basePlayer.metabolism.hydration.value += amount;
            basePlayer.metabolism.oxygen.value = 1;
            basePlayer.metabolism.poison.value = 0;
            basePlayer.metabolism.radiation_level.value = 0;
            basePlayer.metabolism.radiation_poison.value = 0;
            basePlayer.metabolism.temperature.value = 32;
            basePlayer.metabolism.wetness.value = 0;
            basePlayer.StopWounded();
#endif
#if DEBUG
            Puts($"{target.Name} - Heal amount: {amount}");
            Puts($"{target.Name} - Pre health: {health}");
            Puts($"{target.Name} - Post health: {target.Health}");
            Puts($"{target.Name} - Pre health + amount: {health + amount}");
#endif
            if (health == target.MaxHealth)
            {
                return null;
            }
            else if (target.Health >= Math.Abs(health + amount))
            {
#if DEBUG
                Puts($"{target.Name} - Healed? {target.Health >= Math.Abs(health + amount)}");
#endif
                return amount;
            }

            return false;
        }

        #region Heal Command

        private void CommandHeal(IPlayer player, string command, string[] args)
        {
            if (player.IsServer)
            {
                Message(player, "PlayersOnly", command);
                return;
            }

            if (config.UsePermissions && !player.HasPermission(permSelf))
            {
                Message(player, "NotAllowed", command);
                return;
            }

            if (HasCooldown(player, command))
            {
                return;
            }

            float amount = args.Length >= 1 && float.TryParse(args.Last(), out amount) && amount > 0f ? amount : config.MaxHealAmount;
            object healed = HealPlayer(player, amount);
            if (healed is float && (float)healed > 0f)
            {
                if (config.NotifyTarget)
                {
                    Message(player, "YouWereHealed", Math.Round((float)healed, 2));
                }
                if (config.CommandCooldown > 0)
                {
                    cooldowns[player.Id] = Interface.Oxide.Now;
                }
            }
            else if (healed == null)
            {
                Message(player, "YouAreAtMaxHealth", player.Name.Sanitize());
            }
            else
            {
                Message(player, "YouWereNotHealed", player.Name.Sanitize());
            }
        }

        #endregion Heal Command

        #region Heal Player Command

        private void CommandHealPlayer(IPlayer player, string command, string[] args)
        {
            if (!config.UsePermissions && !player.IsAdmin || !player.HasPermission(permPlayer))
            {
                Message(player, "NotAllowed", command);
                return;
            }

            if (HasCooldown(player, command))
            {
                return;
            }

            if (args.Length < 1)
            {
                Message(player, "UsageHealPlayer", command);
                return;
            }

            IPlayer target = FindPlayer(args[0], player);
            if (target == null)
            {
                return;
            }

            float amount = args.Length > 1 && float.TryParse(args.Last(), out amount) && amount > 0f ? amount : config.MaxHealAmount;
            object healed = HealPlayer(target, amount);
            if (healed is float && (float)healed > 0f)
            {
                if (config.CommandCooldown > 0 && !player.HasPermission(permNoCooldown))
                {
                    cooldowns[player.Id] = Interface.Oxide.Now;
                }
                if (!Equals(target.Id, player.Id))
                {
                    Message(player, "PlayerWasHealed", target.Name.Sanitize(), Math.Round((float)healed, 2));
                }
            }
            else if (healed == null)
            {
                Message(player, "PlayerAtMaxHealth", target.Name.Sanitize());
            }
            else
            {
                Message(player, "PlayerWasNotHealed", target.Name.Sanitize());
            }
        }

        #endregion Heal Player Command

        #region Heal All Command

        private void CommandHealAll(IPlayer player, string command, string[] args)
        {
            if (!config.UsePermissions && !player.IsAdmin || !player.HasPermission(permAll))
            {
                Message(player, "NotAllowed", command);
                return;
            }

            if (HasCooldown(player, command))
            {
                return;
            }

            if (!players.Connected.Any())
            {
                Message(player, "NoPlayersOnline");
                return;
            }

            float amount = args.Length > 1 && float.TryParse(args.Last(), out amount) && amount > 0f ? amount : config.MaxHealAmount;
            foreach (IPlayer target in players.Connected)
            {
                HealPlayer(target, amount);
            }
            if (config.CommandCooldown > 0 && !player.HasPermission(permNoCooldown))
            {
                cooldowns[player.Id] = Interface.Oxide.Now;
            }
            Message(player, "PlayersHealed", amount);
        }

        #endregion Heal All Command

        #region Helpers

        private void AddLocalizedCommand(string command)
        {
            foreach (string language in lang.GetLanguages(this))
            {
                Dictionary<string, string> messages = lang.GetMessages(language, this);
                foreach (KeyValuePair<string, string> message in messages)
                {
                    if (message.Key.Equals(command))
                    {
                        if (!string.IsNullOrEmpty(message.Value))
                        {
                            AddCovalenceCommand(message.Value, command);
                        }
                    }
                }
            }
        }

        private IPlayer FindPlayer(string playerNameOrId, IPlayer player)
        {
            IPlayer[] foundPlayers = players.FindPlayers(playerNameOrId).ToArray();
            if (foundPlayers.Length > 1)
            {
                Message(player, "PlayersFound", string.Join(", ", foundPlayers.Select(p => p.Name).Take(10).ToArray()).Truncate(60));
                return null;
            }

            IPlayer target = foundPlayers.Length == 1 ? foundPlayers[0] : null;
            if (target == null)
            {
                Message(player, "NoPlayersFound", playerNameOrId);
                return null;
            }

            return target;
        }

        private string GetLang(string langKey, string playerId = null, params object[] args)
        {
            return string.Format(lang.GetMessage(langKey, this, playerId), args);
        }

        private void Message(IPlayer player, string textOrLang, params object[] args)
        {
            if (player.IsConnected)
            {
                string message = GetLang(textOrLang, player.Id, args);
                player.Reply(message != textOrLang ? message : textOrLang);
            }
        }

        private void MigratePermission(string oldPerm, string newPerm)
        {
            foreach (string groupName in permission.GetPermissionGroups(oldPerm))
            {
                permission.GrantGroupPermission(groupName, newPerm, null);
                permission.RevokeGroupPermission(groupName, oldPerm);
            }

            foreach (string playerId in permission.GetPermissionUsers(oldPerm))
            {
                permission.GrantUserPermission(Regex.Replace(playerId, "[^0-9]", ""), newPerm, null);
                permission.RevokeUserPermission(Regex.Replace(playerId, "[^0-9]", ""), oldPerm);
            }
        }

        #endregion Helpers
    }
}