/*   
TShock, a server mod for Terraria
Copyright (C) 2011 The TShock Team

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using Terraria;
using TerrariaAPI;

namespace TShockAPI
{
    public class Tools
    {
        public static Random Random = new Random();
        //private static List<Group> groups = new List<Group>();

        /// <summary>
        /// Provides the real IP address from a RemoteEndPoint string that contains a port and an IP
        /// </summary>
        /// <param name="mess">A string IPv4 address in IP:PORT form.</param>
        /// <returns>A string IPv4 address.</returns>
        public static string GetRealIP(string mess)
        {
            return mess.Split(':')[0];
        }

        /// <summary>
        /// Used for some places where a list of players might be used.
        /// </summary>
        /// <returns>String of players seperated by commas.</returns>
        public static string GetPlayers()
        {
            var sb = new StringBuilder();
            foreach (TSPlayer player in TShock.Players)
            {
                if (player != null && player.Active)
                {
                    if (sb.Length != 0)
                    {
                        sb.Append(", ");
                    }
                    sb.Append(player.Name);
                }
            }
            return sb.ToString();
        }

        /// <summary>
        /// Finds a player and gets IP as string
        /// </summary>
        /// <param name="msg">Player name</param>
        public static string GetPlayerIP(string playername)
        {
            foreach (TSPlayer player in TShock.Players)
            {
                if (player != null && player.Active)
                {
                    if (playername.ToLower() == player.Name.ToLower())
                    {
                        return player.IP;
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// It's a clamp function
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value">Value to clamp</param>
        /// <param name="max">Maximum bounds of the clamp</param>
        /// <param name="min">Minimum bounds of the clamp</param>
        /// <returns></returns>
        public static T Clamp<T>(T value, T max, T min)
            where T : IComparable<T>
        {
            T result = value;
            if (value.CompareTo(max) > 0)
                result = max;
            if (value.CompareTo(min) < 0)
                result = min;
            return result;
        }

        /// <summary>
        /// Saves the map data
        /// </summary>
        public static void SaveWorld()
        {
            WorldGen.saveWorld();
            Broadcast("World saved.", Color.Yellow);
            Log.Info(string.Format("World saved at ({0})", Main.worldPathName));
        }

        /// <summary>
        /// Broadcasts a message to all players
        /// </summary>
        /// <param name="msg">string message</param>
        public static void Broadcast(string msg)
        {
            Broadcast(msg, Color.Green);
        }

        public static void Broadcast(string msg, byte red, byte green, byte blue)
        {
            TSPlayer.All.SendMessage(msg, red, green, blue);
            TSPlayer.Server.SendMessage(msg, red, green, blue);
            Log.Info(string.Format("Broadcast: {0}", msg));

        }

        public static void Broadcast(string msg, Color color)
        {
            Broadcast(msg, color.R, color.G, color.B);
        }

        /// <summary>
        /// Sends message to all users with 'logs' permission.
        /// </summary>
        /// <param name="log"></param>
        /// <param name="color"></param>
        public static void SendLogs(string log, Color color)
        {
            Log.Info(log);
            TSPlayer.Server.SendMessage(log, color);
            foreach (TSPlayer player in TShock.Players)
            {
                if (player != null && player.Active && player.Group.HasPermission(Permissions.logs) && player.DisplayLogs && TShock.Config.DisableSpewLogs == false)
                    player.SendMessage(log, color);
            }
        }

        /// <summary>
        /// The number of active players on the server.
        /// </summary>
        /// <returns>int playerCount</returns>
        public static int ActivePlayers()
        {
            int num = 0;
            foreach (TSPlayer player in TShock.Players)
            {
                if (player != null && player.Active)
                {
                    num++;
                }
            }
            return num;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="ply"></param>
        /// <returns></returns>
        public static List<TSPlayer> FindPlayer(string ply)
        {
            var found = new List<TSPlayer>();
            ply = ply.ToLower();
            foreach (TSPlayer player in TShock.Players)
            {
                if (player == null)
                    continue;

                string name = player.Name.ToLower();
                if (name.Equals(ply))
                    return new List<TSPlayer> { player };
                if (name.Contains(ply))
                    found.Add(player);
            }
            return found;
        }

        public static void GetRandomClearTileWithInRange(int startTileX, int startTileY, int tileXRange, int tileYRange, out int tileX, out int tileY)
        {
            int j = 0;
            do
            {
                if (j == 100)
                {
                    tileX = startTileX;
                    tileY = startTileY;
                    break;
                }

                tileX = startTileX + Random.Next(tileXRange * -1, tileXRange);
                tileY = startTileY + Random.Next(tileYRange * -1, tileYRange);
                j++;
            }
            while (TileValid(tileX, tileY) && !TileClear(tileX, tileY));
        }

        private static bool TileValid(int tileX, int tileY)
        {
            return tileX >= 0 && tileX <= Main.maxTilesX && tileY >= 0 && tileY <= Main.maxTilesY;
        }

        private static bool TileClear(int tileX, int tileY)
        {
            return !Main.tile[tileX, tileY].active;
        }

        public static List<Item> GetItemByIdOrName(string idOrName)
        {
            int type = -1;
            if (int.TryParse(idOrName, out type))
            {
                return new List<Item> { GetItemById(type) };
            }
            return GetItemByName(idOrName);
        }

        public static Item GetItemById(int id)
        {
            Item item = new Item();
            item.SetDefaults(id);
            return item;
        }

        public static List<Item> GetItemByName(string name)
        {
            //Method #1 - must be exact match, allows support for different pickaxes/hammers/swords etc
            for (int i = 1; i < Main.maxItemTypes; i++)
            {
                Item item = new Item();
                item.SetDefaults(name);
                if (item.name == name)
                    return new List<Item> { item };
            }
            //Method #2 - allows impartial matching
            var found = new List<Item>();
            for (int i = 1; i < Main.maxItemTypes; i++)
            {
                try
                {
                    Item item = new Item();
                    item.SetDefaults(i);
                    if (item.name.ToLower() == name.ToLower())
                        return new List<Item> { item };
                    if (item.name.ToLower().StartsWith(name.ToLower()))
                        found.Add(item);
                }
                catch { }
            }
            return found;
        }

        public static List<NPC> GetNPCByIdOrName(string idOrName)
        {
            int type = -1;
            if (int.TryParse(idOrName, out type))
            {
                return new List<NPC> { GetNPCById(type) };
            }
            return GetNPCByName(idOrName);
        }

        public static NPC GetNPCById(int id)
        {
            NPC npc = new NPC();
            npc.SetDefaults(id);
            return npc;
        }

        public static List<NPC> GetNPCByName(string name)
        {
            //Method #1 - must be exact match, allows support for different coloured slimes
            for (int i = 1; i < Main.maxNPCTypes; i++)
            {
                NPC npc = new NPC();
                npc.SetDefaults(name);
                if (npc.name == name)
                    return new List<NPC> { npc };
            }
            //Method #2 - allows impartial matching
            var found = new List<NPC>();
            for (int i = 1; i < Main.maxNPCTypes; i++)
            {
                NPC npc = new NPC();
                npc.SetDefaults(i);
                if (npc.name.ToLower() == name.ToLower())
                    return new List<NPC> { npc };
                if (npc.name.ToLower().StartsWith(name.ToLower()))
                    found.Add(npc);
            }
            return found;
        }

        public static string GetBuffName(int id)
        {
            return (id > 0 && id < Main.maxBuffs) ? Main.buffName[id] : "null";
        }
        public static string GetBuffDescription(int id)
        {
            return (id > 0 && id < Main.maxBuffs) ? Main.buffTip[id] : "null";
        }
        public static List<int> GetBuffByName(string name)
        {
            for (int i = 1; i < Main.maxBuffs; i++)
            {
                if (Main.buffName[i].ToLower() == name)
                    return new List<int> { i };
            }
            var found = new List<int>();
            for (int i = 1; i < Main.maxBuffs; i++)
            {
                if (Main.buffName[i].ToLower().StartsWith(name.ToLower()))
                    found.Add(i);
            }
            return found;
        }

        /// <summary>
        /// Kicks all player from the server without checking for immunetokick permission.
        /// </summary>
        /// <param name="ply">int player</param>
        /// <param name="reason">string reason</param>
        public static void ForceKickAll(string reason)
        {
            foreach (TSPlayer player in TShock.Players)
            {
                if (player != null && player.Active)
                {
                    ForceKick(player, reason);
                }
            }
        }

        /// <summary>
        /// Kicks a player from the server without checking for immunetokick permission.
        /// </summary>
        /// <param name="ply">int player</param>
        /// <param name="reason">string reason</param>
        public static void ForceKick(TSPlayer player, string reason)
        {
            if (!player.ConnectionAlive)
                return;
            player.Disconnect(reason);
            Log.ConsoleInfo(string.Format("{0} was force kicked for : {1}", player.IP, reason));
        }

        /// <summary>
        /// Kicks a player from the server.
        /// </summary>
        /// <param name="ply">int player</param>
        /// <param name="reason">string reason</param>
        public static bool Kick(TSPlayer player, string reason, string adminUserName = "")
        {
            if (!player.ConnectionAlive)
                return true;
            if (!player.Group.HasPermission(Permissions.immunetokick))
            {
                string playerName = player.Name;
                player.Disconnect(string.Format("Kicked: {0}", reason));
                Log.ConsoleInfo(string.Format("Kicked {0} for : {1}", playerName, reason));
                if (adminUserName.Length == 0)
                    Broadcast(string.Format("{0} was kicked for {1}", playerName, reason.ToLower()));
                else
                    Broadcast(string.Format("{0} kicked {1} for {2}", adminUserName, playerName, reason.ToLower()));
                return true;
            }
            return false;
        }

        /// <summary>
        /// Bans and kicks a player from the server.
        /// </summary>
        /// <param name="ply">int player</param>
        /// <param name="reason">string reason</param>
        public static bool Ban(TSPlayer player, string reason, string adminUserName = "")
        {
            if (!player.ConnectionAlive)
                return true;
            if (!player.Group.HasPermission(Permissions.immunetoban))
            {
                string ip = player.IP;
                string playerName = player.Name;
                TShock.Bans.AddBan(ip, playerName, reason);
                player.Disconnect(string.Format("Banned: {0}", reason));
                Log.ConsoleInfo(string.Format("Banned {0} for : {1}", playerName, reason));
                if (adminUserName.Length == 0)
                    Broadcast(string.Format("{0} was banned for {1}", playerName, reason.ToLower()));
                else
                    Broadcast(string.Format("{0} banned {1} for {2}", adminUserName, playerName, reason.ToLower()));
                return true;
            }
            return false;
        }

        public static bool HandleCheater(TSPlayer player, string reason)
        {
            return HandleBadPlayer(player, "ignorecheatdetection", TShock.Config.BanCheaters, TShock.Config.KickCheaters, reason);
        }

        public static bool HandleGriefer(TSPlayer player, string reason)
        {
            return HandleBadPlayer(player, Permissions.ignoregriefdetection, TShock.Config.BanGriefers, TShock.Config.KickGriefers, reason);
        }

        public static bool HandleTntUser(TSPlayer player, string reason)
        {
            return HandleBadPlayer(player, Permissions.ignoregriefdetection, TShock.Config.BanKillTileAbusers, TShock.Config.KickKillTileAbusers, reason);
        }

        public static bool HandleExplosivesUser(TSPlayer player, string reason)
        {
            return HandleBadPlayer(player, Permissions.ignoregriefdetection, TShock.Config.BanExplosives, TShock.Config.KickExplosives, reason);
        }

        private static bool HandleBadPlayer(TSPlayer player, string overridePermission, bool ban, bool kick, string reason)
        {
            if (!player.Group.HasPermission(overridePermission))
            {
                if (ban)
                {
                    return Ban(player, reason);
                }
                if (kick)
                {
                    return Kick(player, reason);
                }
            }
            return false;
        }

        /// <summary>
        /// Shows a file to the user.
        /// </summary>
        /// <param name="ply">int player</param>
        /// <param name="file">string filename reletave to savedir</param>
        //Todo: Fix this
        public static void ShowFileToUser(TSPlayer player, string file)
        {
            string foo = "";
            using (var tr = new StreamReader(Path.Combine(TShock.SavePath, file)))
            {
                while ((foo = tr.ReadLine()) != null)
                {
                    foo = foo.Replace("%map%", Main.worldName);
                    foo = foo.Replace("%players%", GetPlayers());
                    if (foo.Substring(0, 1) == "%" && foo.Substring(12, 1) == "%") //Look for a beginning color code.
                    {
                        string possibleColor = foo.Substring(0, 13);
                        foo = foo.Remove(0, 13);
                        float[] pC = { 0, 0, 0 };
                        possibleColor = possibleColor.Replace("%", "");
                        string[] pCc = possibleColor.Split(',');
                        if (pCc.Length == 3)
                        {
                            try
                            {
                                player.SendMessage(foo, (byte)Convert.ToInt32(pCc[0]), (byte)Convert.ToInt32(pCc[1]),
                                                   (byte)Convert.ToInt32(pCc[2]));
                                continue;
                            }
                            catch (Exception e)
                            {
                                Log.Error(e.ToString());
                            }
                        }
                    }
                    player.SendMessage(foo);
                }
            }
        }

        /// <summary>
        /// Returns a Group from the name of the group
        /// </summary>
        /// <param name="ply">string groupName</param>
        public static Group GetGroup(string groupName)
        {
            //first attempt on cached groups
            for (int i = 0; i < TShock.Groups.groups.Count; i++)
            {
                if (TShock.Groups.groups[i].Name.Equals(groupName))
                {
                    return TShock.Groups.groups[i];
                }
            }
            return new Group("default");
        }

        /// <summary>
        /// Returns an IPv4 address from a DNS query
        /// </summary>
        /// <param name="hostname">string ip</param>
        public static string GetIPv4Address(string hostname)
        {
            try
            {
                //Get the ipv4 address from GetHostAddresses, if an ip is passed it will return that ip
                var ip = Dns.GetHostAddresses(hostname).FirstOrDefault(i => i.AddressFamily == AddressFamily.InterNetwork);
                //if the dns query was successful then return it, otherwise return an empty string
                return ip != null ? ip.ToString() : "";
            }
            catch (SocketException)
            {
            }
            return "";
        }

        public static string HashAlgo = "md5";

        public static readonly Dictionary<string, Func<HashAlgorithm>> HashTypes = new Dictionary<string, Func<HashAlgorithm>>
        {
            {"sha512", () => new SHA512Managed()},
            {"sha256", () => new SHA256Managed()},
            {"md5", () => new MD5Cng()},
            {"sha512-xp", () => SHA512.Create()},
            {"sha256-xp", () => SHA256.Create()},
            {"md5-xp", () => MD5.Create()},
        };

        /// <summary>
        /// Returns a Sha256 string for a given string
        /// </summary>
        /// <param name="bytes">bytes to hash</param>
        /// <returns>string sha256</returns>
        public static string HashPassword(byte[] bytes)
        {
            if (bytes == null)
                throw new NullReferenceException("bytes");
            Func<HashAlgorithm> func;
            if (!HashTypes.TryGetValue(HashAlgo.ToLower(), out func))
                throw new NotSupportedException("Hashing algorithm {0} is not supported".SFormat(HashAlgo.ToLower()));

            using (var hash = func())
            {
                var ret = hash.ComputeHash(bytes);
                return ret.Aggregate("", (s, b) => s + b.ToString("X2"));
            }
        }
        /// <summary>
        /// Returns a Sha256 string for a given string
        /// </summary>
        /// <param name="bytes">bytes to hash</param>
        /// <returns>string sha256</returns>
        public static string HashPassword(string password)
        {
            if (string.IsNullOrEmpty(password) || password == "non-existant password")
                return "non-existant password";
            return HashPassword(Encoding.UTF8.GetBytes(password));
        }

        /// <summary>
        /// Checks if the string contains any unprintable characters
        /// </summary>
        /// <param name="str">String to check</param>
        /// <returns>True if the string only contains printable characters</returns>
        public static bool ValidString(string str)
        {
            foreach (var c in str)
            {
                if (c < 0x20 || c > 0xA9)
                    return false;
            }
            return true;
        }

        /// <summary>
        /// Checks if world has hit the max number of chests
        /// </summary>
        /// <returns>True if the entire chest array is used</returns>
        public static bool MaxChests()
        {
            for (int i = 0; i < Main.chest.Length; i++)
            {
                if (Main.chest[i] == null)
                    return false;
            }
            return true;
        }
    }
}