/*
TShock, a server mod for Terraria
Copyright (C) 2011-2015 Nyx Studios (fka. 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;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using HttpServer;
using Rests;
using Terraria;
using TShockAPI.DB;

namespace TShockAPI
{
	[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
	public class Permission : Attribute
	{
		public string Name { get; set; }

		public Permission(string name)
		{
			Name = name;
		}
	}

	[AttributeUsage(AttributeTargets.Method)]
	public class RouteAttribute : Attribute
	{
		public string Route { get; set; }

		public RouteAttribute(string route)
		{
			Route = route;
		}
	}

	public class ParameterAttribute : Attribute
	{
		public string Name { get; set; }
		public bool Required { get; set; }
		public string Description { get; set; }
		public Type ArgumentType { get; set; }

		public ParameterAttribute(string name, bool req, string desc, Type type)
		{
			Name = name;
			Required = req;
			Description = desc;
			ArgumentType = type;
		}
	}

	[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
	public class Noun : ParameterAttribute
	{
		public Noun(string name, bool req, string desc, Type type) : base(name, req, desc, type) { }
	}

	[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
	public class Verb : ParameterAttribute
	{
		public Verb(string name, string desc, Type type) : base(name, true, desc, type) { }
	}

	[AttributeUsage(AttributeTargets.Method)]
	public class Token : Noun
	{
		public Token() : base("token", true, "The REST authentication token.", typeof(String)){}
	}

	public class RestManager
	{
		private Rest Rest;

		public RestManager(Rest rest)
		{
			Rest = rest;
		}

		public void RegisterRestfulCommands()
		{
			// Server Commands
			if (TShock.Config.EnableTokenEndpointAuthentication)
			{
				Rest.Register(new SecureRestCommand("/v2/server/status", ServerStatusV2));
				Rest.Register(new SecureRestCommand("/status", ServerStatus));
				Rest.Register(new SecureRestCommand("/v3/server/motd", ServerMotd));
				Rest.Register(new SecureRestCommand("/v3/server/rules", ServerRules));
			}
			else
			{
				Rest.Register(new RestCommand("/v2/server/status", (a) => this.ServerStatusV2(new RestRequestArgs(a.Verbs, a.Parameters, a.Request, SecureRest.TokenData.None, a.Context))));
				Rest.Register(new RestCommand("/status", (a) => this.ServerStatus(new RestRequestArgs(a.Verbs, a.Parameters, a.Request, SecureRest.TokenData.None, a.Context))));
				Rest.Register(new RestCommand("/v3/server/motd", (a) => this.ServerMotd(new RestRequestArgs(a.Verbs, a.Parameters, a.Request, SecureRest.TokenData.None, a.Context))));
				Rest.Register(new RestCommand("/v3/server/rules", (a) => this.ServerRules(new RestRequestArgs(a.Verbs, a.Parameters, a.Request, SecureRest.TokenData.None, a.Context))));
			}

			Rest.Register(new SecureRestCommand("/v2/server/broadcast", ServerBroadcast));
			Rest.Register(new SecureRestCommand("/v3/server/reload", ServerReload, RestPermissions.restcfg));
			Rest.Register(new SecureRestCommand("/v2/server/off", ServerOff, RestPermissions.restmaintenance));
			Rest.Register(new SecureRestCommand("/v3/server/restart", ServerRestart, RestPermissions.restmaintenance));
			Rest.Register(new SecureRestCommand("/v2/server/rawcmd", ServerCommand, RestPermissions.restrawcommand));
			Rest.Register(new SecureRestCommand("/v3/server/rawcmd", ServerCommandV3, RestPermissions.restrawcommand));
			Rest.Register(new SecureRestCommand("/tokentest", ServerTokenTest));

			// User Commands
			Rest.Register(new SecureRestCommand("/v2/users/activelist", UserActiveListV2, RestPermissions.restviewusers));
			Rest.Register(new SecureRestCommand("/v2/users/create", UserCreateV2, RestPermissions.restmanageusers) { DoLog = false });
			Rest.Register(new SecureRestCommand("/v2/users/list", UserListV2, RestPermissions.restviewusers));
			Rest.Register(new SecureRestCommand("/v2/users/read", UserInfoV2, RestPermissions.restviewusers));
			Rest.Register(new SecureRestCommand("/v2/users/destroy", UserDestroyV2, RestPermissions.restmanageusers));
			Rest.Register(new SecureRestCommand("/v2/users/update", UserUpdateV2, RestPermissions.restmanageusers) { DoLog = false });

			// Ban Commands
			Rest.Register(new SecureRestCommand("/bans/create", BanCreate, RestPermissions.restmanagebans));
			Rest.Register(new SecureRestCommand("/v2/bans/list", BanListV2, RestPermissions.restviewbans));
			Rest.Register(new SecureRestCommand("/v2/bans/read", BanInfoV2, RestPermissions.restviewbans));
			Rest.Register(new SecureRestCommand("/v2/bans/destroy", BanDestroyV2, RestPermissions.restmanagebans));

			// World Commands
			Rest.Register(new SecureRestCommand("/world/read", WorldRead));
			Rest.Register(new SecureRestCommand("/world/meteor", WorldMeteor, RestPermissions.restcauseevents));
			Rest.Register(new SecureRestCommand("/world/bloodmoon/{bloodmoon}", WorldBloodmoon, RestPermissions.restcauseevents));
			Rest.Register(new SecureRestCommand("/v2/world/save", WorldSave, RestPermissions.restcfg));
			Rest.Register(new SecureRestCommand("/v2/world/autosave/state/{state}", WorldChangeSaveSettings, RestPermissions.restcfg));
			Rest.Register(new SecureRestCommand("/v2/world/butcher", WorldButcher, RestPermissions.restbutcher));

			// Player Commands
			Rest.Register(new SecureRestCommand("/lists/players", PlayerList));
			Rest.Register(new SecureRestCommand("/v2/players/list", PlayerListV2));
			Rest.Register(new SecureRestCommand("/v2/players/read", PlayerReadV2, RestPermissions.restuserinfo));
			Rest.Register(new SecureRestCommand("/v3/players/read", PlayerReadV3, RestPermissions.restuserinfo));
			Rest.Register(new SecureRestCommand("/v2/players/kick", PlayerKickV2, RestPermissions.restkick));
			Rest.Register(new SecureRestCommand("/v2/players/ban", PlayerBanV2, RestPermissions.restban, RestPermissions.restmanagebans));
			Rest.Register(new SecureRestCommand("/v2/players/kill", PlayerKill, RestPermissions.restkill));
			Rest.Register(new SecureRestCommand("/v2/players/mute", PlayerMute, RestPermissions.restmute));
			Rest.Register(new SecureRestCommand("/v2/players/unmute", PlayerUnMute, RestPermissions.restmute));

			// Group Commands
			Rest.Register(new SecureRestCommand("/v2/groups/list", GroupList, RestPermissions.restviewgroups));
			Rest.Register(new SecureRestCommand("/v2/groups/read", GroupInfo, RestPermissions.restviewgroups));
			Rest.Register(new SecureRestCommand("/v2/groups/destroy", GroupDestroy, RestPermissions.restmanagegroups));
			Rest.Register(new SecureRestCommand("/v2/groups/create", GroupCreate, RestPermissions.restmanagegroups));
			Rest.Register(new SecureRestCommand("/v2/groups/update", GroupUpdate, RestPermissions.restmanagegroups));
		}

		#region RestServerMethods

		[Description("Executes a remote command on the server, and returns the output of the command.")]
		[RouteAttribute("/v2/server/rawcmd")]
		[Permission(RestPermissions.restrawcommand)]
		[Noun("cmd", true, "The command and arguments to execute.", typeof(String))]
		[Token]
		private object ServerCommand(RestRequestArgs args)
		{
			if (string.IsNullOrWhiteSpace(args.Parameters["cmd"]))
				return RestMissingParam("cmd");

			Group restPlayerGroup;
			// TODO: Get rid of this when the old REST permission model is removed.
			if (TShock.Config.RestUseNewPermissionModel)
				restPlayerGroup = TShock.Groups.GetGroupByName(args.TokenData.UserGroupName);
			else
				restPlayerGroup = new SuperAdminGroup();

			TSRestPlayer tr = new TSRestPlayer(args.TokenData.Username, restPlayerGroup);
			Commands.HandleCommand(tr, args.Parameters["cmd"]);
			return RestResponse(string.Join("\n", tr.GetCommandOutput()));
		}

		[Description("Executes a remote command on the server, and returns the output of the command.")]
		[RouteAttribute("/v3/server/rawcmd")]
		[Permission(RestPermissions.restrawcommand)]
		[Noun("cmd", true, "The command and arguments to execute.", typeof(String))]
		[Token]
		private object ServerCommandV3(RestRequestArgs args)
		{
			if (string.IsNullOrWhiteSpace(args.Parameters["cmd"]))
				return RestMissingParam("cmd");

			Group restPlayerGroup;
			// TODO: Get rid of this when the old REST permission model is removed.
			if (TShock.Config.RestUseNewPermissionModel)
				restPlayerGroup = TShock.Groups.GetGroupByName(args.TokenData.UserGroupName);
			else
				restPlayerGroup = new SuperAdminGroup();

			TSRestPlayer tr = new TSRestPlayer(args.TokenData.Username, restPlayerGroup);
			Commands.HandleCommand(tr, args.Parameters["cmd"]);
			return new RestObject()
			{
				{"response", tr.GetCommandOutput()}
			};
		}

		[Description("Turn the server off.")]
		[Route("/v2/server/off")]
		[Permission(RestPermissions.restmaintenance)]
		[Noun("confirm", true, "Required to confirm that actually want to turn the server off.", typeof(bool))]
		[Noun("message", false, "The shutdown message.", typeof(String))]
		[Noun("nosave", false, "Shutdown without saving.", typeof(bool))]
		[Token]
		private object ServerOff(RestRequestArgs args)
		{
			if (!GetBool(args.Parameters["confirm"], false))
				return RestInvalidParam("confirm");

			// Inform players the server is shutting down
			var reason = string.IsNullOrWhiteSpace(args.Parameters["message"]) ? "Server is shutting down" : args.Parameters["message"];
			TShock.Utils.StopServer(!GetBool(args.Parameters["nosave"], false), reason);

			return RestResponse("The server is shutting down");
		}

		[Description("Attempt to restart the server.")]
		[Route("/v3/server/restart")]
		[Permission(RestPermissions.restmaintenance)]
		[Noun("confirm", true, "Confirm that you actually want to restart the server", typeof(bool))]
		[Noun("message", false, "The shutdown message.", typeof(String))]
		[Noun("nosave", false, "Shutdown without saving.", typeof(bool))]
		[Token]
		private object ServerRestart(RestRequestArgs args)
		{
			if (!GetBool(args.Parameters["confirm"], false))
				return RestInvalidParam("confirm");

			// Inform players the server is shutting down
			var reason = string.IsNullOrWhiteSpace(args.Parameters["message"]) ? "Server is restarting" : args.Parameters["message"];
			TShock.Utils.RestartServer(!GetBool(args.Parameters["nosave"], false), reason);

			return RestResponse("The server is shutting down and will attempt to restart");
		}

		[Description("Reload config files for the server.")]
		[Route("/v3/server/reload")]
		[Permission(RestPermissions.restcfg)]
		[Token]
		private object ServerReload(RestRequestArgs args)
		{
			TShock.Utils.Reload(new TSRestPlayer(args.TokenData.Username, TShock.Groups.GetGroupByName(args.TokenData.UserGroupName)));

			return RestResponse("Configuration, permissions, and regions reload complete. Some changes may require a server restart.");
		}

		[Description("Broadcast a server wide message.")]
		[Route("/v2/server/broadcast")]
		[Noun("msg", true, "The message to broadcast.", typeof(String))]
		[Token]
		private object ServerBroadcast(RestRequestArgs args)
		{
			var msg = args.Parameters["msg"];
			if (string.IsNullOrWhiteSpace(msg))
				return RestMissingParam("msg");
			TSPlayer.All.SendInfoMessage(msg);
			return RestResponse("The message was broadcasted successfully");
		}

		[Description("Returns the motd, if it exists.")]
		[Route("/v3/server/motd")]
		[Token]
		private object ServerMotd(RestRequestArgs args)
		{
			string motdFilePath = Path.Combine(TShock.SavePath, "motd.txt");
			if (!File.Exists(motdFilePath))
				return this.RestError("The motd.txt was not found.", "500");

			return new RestObject()
			{
				{"motd", File.ReadAllLines(motdFilePath)}
			};
		}

		[Description("Returns the rules, if they exist.")]
		[Route("/v3/server/rules")]
		[Token]
		private object ServerRules(RestRequestArgs args)
		{
			string rulesFilePath = Path.Combine(TShock.SavePath, "rules.txt");
			if (!File.Exists(rulesFilePath))
				return this.RestError("The rules.txt was not found.", "500");

			return new RestObject()
			{
				{"rules", File.ReadAllLines(rulesFilePath)}
			};
		}

		[Description("Returns the current status of the server.")]
		[Route("/status")]
		[Token]
		private object ServerStatus(RestRequestArgs args)
		{
			var activeplayers = Main.player.Where(p => null != p && p.active).ToList();
			return new RestObject()
			{
				{"name", TShock.Config.ServerName},
				{"port", Convert.ToString(Netplay.ListenPort)},
				{"playercount", Convert.ToString(activeplayers.Count())},
				{"players", string.Join(", ", activeplayers.Select(p => p.name))},
			};
		}

		[Description("Get a list of information about the current TShock server.")]
		[Route("/v2/server/status")]
		[Token]
		private object ServerStatusV2(RestRequestArgs args)
		{
			var ret = new RestObject()
			{
				{"name", TShock.Config.ServerName},
				{"serverversion", Main.versionNumber},
				{"tshockversion", TShock.VersionNum},
				{"port", TShock.Config.ServerPort},
				{"playercount", Main.player.Where(p => null != p && p.active).Count()},
				{"maxplayers", TShock.Config.MaxSlots},
				{"world", (TShock.Config.UseServerName ? TShock.Config.ServerName : Main.worldName)},
				{"uptime", (DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime).ToString(@"d'.'hh':'mm':'ss")},
				{"serverpassword", !string.IsNullOrEmpty(TShock.Config.ServerPassword)}
			};

			if (GetBool(args.Parameters["players"], false))
			{
				var players = new ArrayList();
				foreach (TSPlayer tsPlayer in TShock.Players.Where(p => null != p))
				{
					var p = PlayerFilter(tsPlayer, args.Parameters, ((args.TokenData.UserGroupName) != "" && TShock.Utils.GetGroup(args.TokenData.UserGroupName).HasPermission(RestPermissions.viewips)));
					if (null != p)
						players.Add(p);
				}
				ret.Add("players", players);
			}

			if (GetBool(args.Parameters["rules"], false))
			{
				var rules = new Dictionary<string,object>();
				rules.Add("AutoSave", TShock.Config.AutoSave);
				rules.Add("DisableBuild", TShock.Config.DisableBuild);
				rules.Add("DisableClownBombs", TShock.Config.DisableClownBombs);
				rules.Add("DisableDungeonGuardian", TShock.Config.DisableDungeonGuardian);
				rules.Add("DisableInvisPvP", TShock.Config.DisableInvisPvP);
				rules.Add("DisableSnowBalls", TShock.Config.DisableSnowBalls);
				rules.Add("DisableTombstones", TShock.Config.DisableTombstones);
				rules.Add("EnableWhitelist", TShock.Config.EnableWhitelist);
				rules.Add("HardcoreOnly", TShock.Config.HardcoreOnly);
				rules.Add("PvPMode", TShock.Config.PvPMode);
				rules.Add("SpawnProtection", TShock.Config.SpawnProtection);
				rules.Add("SpawnProtectionRadius", TShock.Config.SpawnProtectionRadius);
				rules.Add("ServerSideInventory", Main.ServerSideCharacter);

				ret.Add("rules", rules);
			}
			return ret;
		}

		[Description("Test if a token is still valid.")]
		[Route("/tokentest")]
		[Token]
		private object ServerTokenTest(RestRequestArgs args)
		{
			return new RestObject()
			{
				{"response", "Token is valid and was passed through correctly."},
				{"associateduser", args.TokenData.Username}
			};
		}

		#endregion

		#region RestUserMethods

		[Description("Returns the list of user accounts that are currently in use on the server.")]
		[Route("/v2/users/activelist")]
		[Permission(RestPermissions.restviewusers)]
		[Token]
		private object UserActiveListV2(RestRequestArgs args)
		{
			return new RestObject() { { "activeusers", string.Join("\t", TShock.Players.Where(p => null != p && null != p.User && p.Active).Select(p => p.User.Name)) } };
		}

		[Description("Lists all user accounts in the TShock database.")]
		[Route("/v2/users/list")]
		[Permission(RestPermissions.restviewusers)]
		[Token]
		private object UserListV2(RestRequestArgs args)
		{
			return new RestObject() { { "users", TShock.Users.GetUsers().Select(p => new Dictionary<string,object>(){
				{"name", p.Name},
				{"id", p.ID},
				{"group", p.Group},
			}) } };
		}

		[Description("Create a new TShock user account.")]
		[Route("/v2/users/create")]
		[Permission(RestPermissions.restmanageusers)]
		[Noun("user", true, "The user account name for the new account.", typeof(String))]
		[Noun("group", false, "The group the new account should be assigned.", typeof(String))]
		[Noun("password", true, "The password for the new account.", typeof(String))]
		[Token]
		private object UserCreateV2(RestRequestArgs args)
		{
			var username = args.Parameters["user"];
			if (string.IsNullOrWhiteSpace(username))
				return RestMissingParam("user");

			var group = args.Parameters["group"];
		    if (string.IsNullOrWhiteSpace(group))
		        group = TShock.Config.DefaultRegistrationGroupName;

			var password = args.Parameters["password"];
			if (string.IsNullOrWhiteSpace(password))
				return RestMissingParam("password");

			// NOTE: ip can be blank
			User user = new User(username, password, "", group, "", "", "");
			try
			{
				TShock.Users.AddUser(user);
			}
			catch (Exception e)
			{
				return RestError(e.Message);
			}

			return RestResponse("User was successfully created");
		}

		[Description("Update a users information.")]
		[Route("/v2/users/update")]
		[Permission(RestPermissions.restmanageusers)]
		[Noun("user", true, "The search criteria (name or id of account to lookup).", typeof(String))]
		[Noun("type", true, "The search criteria type (name for name lookup, id for id lookup).", typeof(String))]
		[Noun("password", false, "The users new password, and at least this or group must be defined.", typeof(String))]
		[Noun("group", false, "The new group for the user, at least this or password must be defined.", typeof(String))]
		[Token]
		private object UserUpdateV2(RestRequestArgs args)
		{
			var ret = UserFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			var password = args.Parameters["password"];
			var group = args.Parameters["group"];
			if (string.IsNullOrWhiteSpace(group) && string.IsNullOrWhiteSpace(password))
				return RestMissingParam("group", "password");

			User user = (User)ret;
			var response = new RestObject();
			if (!string.IsNullOrWhiteSpace(password))
			{
				try
				{
					TShock.Users.SetUserPassword(user, password);
					response.Add("password-response", "Password updated successfully");
				}
				catch (Exception e)
				{
					return RestError("Failed to update user password (" + e.Message + ")");
				}
			}

			if (!string.IsNullOrWhiteSpace(group))
			{
				try
				{
					TShock.Users.SetUserGroup(user, group);
					response.Add("group-response", "Group updated successfully");
				}
				catch (Exception e)
				{
					return RestError("Failed to update user group (" + e.Message + ")");
				}
			}

			return response;
		}

		[Description("Destroy a TShock user account.")]
		[Route("/v2/users/destroy")]
		[Permission(RestPermissions.restmanageusers)]
		[Noun("user", true, "The search criteria (name or id of account to lookup).", typeof(String))]
		[Noun("type", true, "The search criteria type (name for name lookup, id for id lookup).", typeof(String))]
		[Token]
		private object UserDestroyV2(RestRequestArgs args)
		{
			var ret = UserFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			try
			{
				TShock.Users.RemoveUser((User)ret);
			}
			catch (Exception e)
			{
				return RestError(e.Message);
			}

			return RestResponse("User deleted successfully");
		}

		[Description("List detailed information for a user account.")]
		[Route("/v2/users/read")]
		[Permission(RestPermissions.restviewusers)]
		[Noun("user", true, "The search criteria (name or id of account to lookup).", typeof(String))]
		[Noun("type", true, "The search criteria type (name for name lookup, id for id lookup).", typeof(String))]
		[Token]
		private object UserInfoV2(RestRequestArgs args)
		{
			var ret = UserFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			User user = (User)ret;
			return new RestObject() { { "group", user.Group }, { "id", user.ID.ToString() }, { "name", user.Name } };
		}

		#endregion

		#region RestBanMethods

		[Description("Create a new ban entry.")]
		[Route("/bans/create")]
		[Permission(RestPermissions.restmanagebans)]
		[Noun("ip", false, "The IP to ban, at least this or name must be specified.", typeof(String))]
		[Noun("name", false, "The name to ban, at least this or ip must be specified.", typeof(String))]
		[Noun("reason", false, "The reason to assign to the ban.", typeof(String))]
		[Token]
		private object BanCreate(RestRequestArgs args)
		{
			var ip = args.Parameters["ip"];
			var name = args.Parameters["name"];

			if (string.IsNullOrWhiteSpace(ip) && string.IsNullOrWhiteSpace(name))
				return RestMissingParam("ip", "name");

			try
			{
				TShock.Bans.AddBan(ip, name, "", args.Parameters["reason"], true, args.TokenData.Username);
			}
			catch (Exception e)
			{
				return RestError(e.Message);
			}
			return RestResponse("Ban created successfully");
		}

		[Description("Delete an existing ban entry.")]
		[Route("/v2/bans/destroy")]
		[Permission(RestPermissions.restmanagebans)]
		[Noun("ban", true, "The search criteria, either an IP address or a name.", typeof(String))]
		[Noun("type", true, "The type of search criteria, 'ip' or 'name'.  Also used as the method of removing from the database.", typeof(String))]
		[Noun("caseinsensitive", false, "Name lookups should be case insensitive.", typeof(bool))]
		[Token]
		private object BanDestroyV2(RestRequestArgs args)
		{
			var ret = BanFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			try
			{
				Ban ban = (Ban)ret;
				switch (args.Parameters["type"])
				{
					case "ip":
						if (!TShock.Bans.RemoveBan(ban.IP, false, false, true))
							return RestResponse("Failed to delete ban (already deleted?)");
						break;
					case "name":
						if (!TShock.Bans.RemoveBan(ban.Name, true, GetBool(args.Parameters["caseinsensitive"], true)))
							return RestResponse("Failed to delete ban (already deleted?)");
						break;
					default:
						return RestError("Invalid Type: '" + args.Parameters["type"] + "'");
				}

			}
			catch (Exception e)
			{
				return RestError(e.Message);
			}

			return RestResponse("Ban deleted successfully");
		}

		[Description("View the details of a specific ban.")]
		[Route("/v2/bans/read")]
		[Permission(RestPermissions.restviewbans)]
		[Noun("ban", true, "The search criteria, either an IP address or a name.", typeof(String))]
		[Noun("type", true, "The type of search criteria, 'ip' or 'name'.", typeof(String))]
		[Noun("caseinsensitive", false, "Name lookups should be case insensitive.", typeof(bool))]
		[Token]
		private object BanInfoV2(RestRequestArgs args)
		{
			var ret = BanFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			Ban ban = (Ban)ret;
			return new RestObject() {
				{"name", null == ban.Name ? "" : ban.Name},
				{"ip", null == ban.IP ? "" : ban.IP},
				{"reason", null == ban.Reason ? "" : ban.Reason},
			};
		}

		[Description("View all bans in the TShock database.")]
		[Route("/v2/bans/list")]
		[Permission(RestPermissions.restviewbans)]
		[Token]
		private object BanListV2(RestRequestArgs args)
		{
			var banList = new ArrayList();
			foreach (var ban in TShock.Bans.GetBans())
			{
				banList.Add(
					new Dictionary<string, string>
					{
						{"name", null == ban.Name ? "" : ban.Name},
						{"ip", null == ban.IP ? "" : ban.IP},
						{"reason", null == ban.Reason ? "" : ban.Reason},
					}
				);
			}

			return new RestObject() { { "bans", banList } };
		}

		#endregion

		#region RestWorldMethods

		[Route("/v2/world/autosave/state/{state}")]
		[Permission(RestPermissions.restcfg)]
		[Verb("state", "The status for autosave.", typeof(bool))]
		[Token]
		private object WorldChangeSaveSettings(RestRequestArgs args)
		{
			bool autoSave;
			if (!bool.TryParse(args.Verbs["state"], out autoSave))
				return RestInvalidParam("state");
			TShock.Config.AutoSave = autoSave;

			return RestResponse("AutoSave has been set to " + autoSave);
		}

		[Description("Save the world.")]
		[Route("/v2/world/save")]
		[Permission(RestPermissions.restcfg)]
		[Token]
		private object WorldSave(RestRequestArgs args)
		{
			SaveManager.Instance.SaveWorld();

			return RestResponse("World saved");
		}

		[Description("Butcher npcs.")]
		[Route("/v2/world/butcher")]
		[Permission(RestPermissions.restbutcher)]
		[Noun("killfriendly", false, "Should friendly npcs be butchered.", typeof(bool))]
		[Token]
		private object WorldButcher(RestRequestArgs args)
		{
			bool killFriendly;
			if (!bool.TryParse(args.Parameters["killfriendly"], out killFriendly))
				return RestInvalidParam("killfriendly");

			int killcount = 0;
			for (int i = 0; i < Main.npc.Length; i++)
			{
				if (Main.npc[i].active && Main.npc[i].type != 0 && !Main.npc[i].townNPC && (!Main.npc[i].friendly || killFriendly))
				{
					TSPlayer.Server.StrikeNPC(i, 99999, 90f, 1);
					killcount++;
				}
			}

			return RestResponse(killcount + " NPCs have been killed");
		}

		[Description("Get information regarding the world.")]
		[Route("/world/read")]
		[Token]
		private object WorldRead(RestRequestArgs args)
		{
			return new RestObject()
			{
				{"name", (TShock.Config.UseServerName ? TShock.Config.ServerName : Main.worldName)},
				{"size", Main.maxTilesX + "*" + Main.maxTilesY},
				{"time", Main.time},
				{"daytime", Main.dayTime},
				{"bloodmoon", Main.bloodMoon},
				{"invasionsize", Main.invasionSize}
			};
		}

		[Description("Drops a meteor on the world.")]
		[Route("/world/meteor")]
		[Permission(RestPermissions.restcauseevents)]
		[Token]
		private object WorldMeteor(RestRequestArgs args)
		{
			if (null == WorldGen.genRand)
				WorldGen.genRand = new Random();
			WorldGen.dropMeteor();
			return RestResponse("Meteor has been spawned");
		}

		[Description("Toggle the status of blood moon.")]
		[Route("/world/bloodmoon/{bloodmoon}")]
		[Permission(RestPermissions.restcauseevents)]
		[Verb("bloodmoon", "State of bloodmoon.", typeof(bool))]
		[Token]
		private object WorldBloodmoon(RestRequestArgs args)
		{
			bool bloodmoon;
			if (!bool.TryParse(args.Verbs["bloodmoon"], out bloodmoon))
				return RestInvalidParam("bloodmoon");
			Main.bloodMoon = bloodmoon;

			return RestResponse("Blood Moon has been set to " + bloodmoon);
		}

		#endregion

		#region RestPlayerMethods

		[Description("Unmute a player.")]
		[Route("/v2/players/unmute")]
		[Permission(RestPermissions.restmute)]
		[Noun("player", true, "The player to mute.", typeof(String))]
		[Token]
		private object PlayerUnMute(RestRequestArgs args)
		{
			return PlayerSetMute(args.Parameters, false);
		}

		[Description("Mute a player.")]
		[Route("/v2/players/mute")]
		[Permission(RestPermissions.restmute)]
		[Noun("player", true, "The player to mute.", typeof(String))]
		[Token]
		private object PlayerMute(RestRequestArgs args)
		{
			return PlayerSetMute(args.Parameters, true);
		}

		[Description("List all player names that are currently on the server.")]
		[Route("/lists/players")]
		[Token]
		private object PlayerList(RestRequestArgs args)
		{
			var activeplayers = Main.player.Where(p => null != p && p.active).ToList();
			return new RestObject() { { "players", string.Join(", ", activeplayers.Select(p => p.name)) } };
		}

		[Description("Fetches detailed user information on all connected users, and can be filtered by specifying a key value pair filter users where the key is a field and the value is a users field value.")]
		[Route("/v2/players/list")]
		[Token]
		private object PlayerListV2(RestRequestArgs args)
		{
			var playerList = new ArrayList();
			foreach (TSPlayer tsPlayer in TShock.Players.Where(p => null != p))
			{
				var p = PlayerFilter(tsPlayer, args.Parameters);
				if (null != p)
					playerList.Add(p);
			}
			return new RestObject() { { "players", playerList } };
		}

		[Description("Get information for a user.")]
		[Route("/v2/players/read")]
		[Permission(RestPermissions.restuserinfo)]
		[Noun("player", true, "The player to lookup", typeof(String))]
		[Token]
		private object PlayerReadV2(RestRequestArgs args)
		{
			var ret = PlayerFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			TSPlayer player = (TSPlayer)ret;
			var activeItems = player.TPlayer.inventory.Where(p => p.active).ToList();
			return new RestObject()
			{
				{"nickname", player.Name},
				{"username", null == player.User ? "" : player.User.Name},
				{"ip", player.IP},
				{"group", player.Group.Name},
				{"registered", null == player.User ? "" : player.User.Registered},
				{"position", player.TileX + "," + player.TileY},
				{"inventory", string.Join(", ", activeItems.Select(p => (p.name + ":" + p.stack)))},
				{"buffs", string.Join(", ", player.TPlayer.buffType)}
			};
		}

		[Description("Get information for a user.")]
		[Route("/v3/players/read")]
		[Permission(RestPermissions.restuserinfo)]
		[Noun("player", true, "The player to lookup", typeof(String))]
		[Token]
		private object PlayerReadV3(RestRequestArgs args)
		{
			var ret = PlayerFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			TSPlayer player = (TSPlayer)ret;
			var inventory = player.TPlayer.inventory.Where(p => p.active).ToList();
			var equipment = player.TPlayer.armor.Where(p => p.active).ToList();
			var dyes = player.TPlayer.dye.Where(p => p.active).ToList();
			return new RestObject()
			{
				{"nickname", player.Name},
				{"username", null == player.User ? "" : player.User.Name},
				{"ip", player.IP},
				{"group", player.Group.Name},
				{"registered", null == player.User ? "" : player.User.Registered},
				{"position", player.TileX + "," + player.TileY},
				{"inventory", string.Join(", ", inventory.Select(p => (p.name + ":" + p.stack)))},
				{"armor", string.Join(", ", equipment.Select(p => (p.netID + ":" + p.prefix)))},
				{"dyes", string.Join(", ", dyes.Select(p => (p.name)))},
				{"buffs", string.Join(", ", player.TPlayer.buffType)}
			};
		}

		[Description("Kick a player off the server.")]
		[Route("/v2/players/kick")]
		[Permission(RestPermissions.restkick)]
		[Noun("player", true, "The player to kick.", typeof(String))]
		[Noun("reason", false, "The reason the player was kicked.", typeof(String))]
		[Token]
		private object PlayerKickV2(RestRequestArgs args)
		{
			var ret = PlayerFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			TSPlayer player = (TSPlayer)ret;
			TShock.Utils.ForceKick(player, null == args.Parameters["reason"] ? "Kicked via web" : args.Parameters["reason"], false, true);
			return RestResponse("Player " + player.Name + " was kicked");
		}

		[Description("Add a ban to the database.")]
		[Route("/v2/players/ban")]
		[Permission(RestPermissions.restban)]
		[Permission(RestPermissions.restmanagebans)]
		[Noun("player", true, "The player to kick.", typeof(String))]
		[Noun("reason", false, "The reason the user was banned.", typeof(String))]
		[Token]
		private object PlayerBanV2(RestRequestArgs args)
		{
			var ret = PlayerFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			TSPlayer player = (TSPlayer)ret;
			var reason = null == args.Parameters["reason"] ? "Banned via web" : args.Parameters["reason"];
			TShock.Bans.AddBan(player.IP, player.Name, "", reason);
			TShock.Utils.ForceKick(player, reason, false, true);
			return RestResponse("Player " + player.Name + " was banned");
		}

		[Description("Kill a player.")]
		[Route("/v2/players/kill")]
		[Permission(RestPermissions.restkill)]
		[Noun("player", true, "The player to kick.", typeof(String))]
		[Noun("from", false, "Who killed the player.", typeof(String))]
		[Token]
		private object PlayerKill(RestRequestArgs args)
		{
			var ret = PlayerFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			TSPlayer player = (TSPlayer)ret;
			player.DamagePlayer(999999);
			var from = string.IsNullOrWhiteSpace(args.Parameters["from"]) ? "Server Admin" : args.Parameters["from"];
			player.SendInfoMessage(string.Format("{0} just killed you!", from));
			return RestResponse("Player " + player.Name + " was killed");
		}

		#endregion

		#region RestGroupMethods

		[Description("View all groups in the TShock database.")]
		[Route("/v2/groups/list")]
		[Permission(RestPermissions.restviewgroups)]
		[Token]
		private object GroupList(RestRequestArgs args)
		{
			var groups = new ArrayList();
			foreach (Group group in TShock.Groups)
			{
				groups.Add(new Dictionary<string, object> {{"name", group.Name}, {"parent", group.ParentName}, {"chatcolor", group.ChatColor}});
			}
			return new RestObject() { { "groups", groups } };
		}

		[Description("Display information of a group.")]
		[Route("/v2/groups/read")]
		[Permission(RestPermissions.restviewgroups)]
		[Noun("group", true, "The group name to get information on.", typeof(String))]
		[Token]
		private object GroupInfo(RestRequestArgs args)
		{
			var ret = GroupFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			Group group = (Group)ret;
			return new RestObject() {
				{"name", group.Name},
				{"parent", group.ParentName},
				{"chatcolor", string.Format("{0},{1},{2}", group.R, group.G, group.B)},
				{"permissions", group.permissions},
				{"negatedpermissions", group.negatedpermissions},
				{"totalpermissions", group.TotalPermissions}
			};
		}

		[Description("Delete a group.")]
		[Route("/v2/groups/destroy")]
		[Permission(RestPermissions.restmanagegroups)]
		[Noun("group", true, "The group name to delete.", typeof(String))]
		[Token]
		private object GroupDestroy(RestRequestArgs args)
		{
			var ret = GroupFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			Group group = (Group)ret;
			try
			{
				TShock.Groups.DeleteGroup(group.Name, true);
			}
			catch (Exception e)
			{
				return RestError(e.Message);
			}

			return RestResponse("Group '" + group.Name + "' deleted successfully");
		}

		[Description("Create a new group.")]
		[Route("/v2/groups/create")]
		[Permission(RestPermissions.restmanagegroups)]
		[Noun("group", true, "The name of the new group.", typeof(String))]
		[Noun("parent", false, "The name of the parent group.", typeof(String))]
		[Noun("permissions", false, "A comma seperated list of permissions for the new group.", typeof(String))]
		[Noun("chatcolor", false, "A r,g,b string representing the color for this groups chat.", typeof(String))]
		[Token]
		private object GroupCreate(RestRequestArgs args)
		{
			var name = args.Parameters["group"];
			if (string.IsNullOrWhiteSpace(name))
				return RestMissingParam("group");
			try
			{
				TShock.Groups.AddGroup(name, args.Parameters["parent"], args.Parameters["permissions"], args.Parameters["chatcolor"]);
			}
			catch (Exception e)
			{
				return RestError(e.Message);
			}

			return RestResponse("Group '" + name + "' created successfully");
		}

		[Route("/v2/groups/update")]
		[Permission(RestPermissions.restmanagegroups)]
		[Noun("group", true, "The name of the group to modify.", typeof(String))]
		[Noun("parent", false, "The name of the new parent for this group.", typeof(String))]
		[Noun("chatcolor", false, "The new chat color r,g,b.", typeof(String))]
		[Noun("permissions", false, "The new comma seperated list of permissions.", typeof(String))]
		[Token]
		private object GroupUpdate(RestRequestArgs args)
		{
			var ret = GroupFind(args.Parameters);
			if (ret is RestObject)
				return ret;

			Group group = (Group)ret;
			var parent = (null == args.Parameters["parent"]) ? group.ParentName : args.Parameters["parent"];
			var chatcolor = (null == args.Parameters["chatcolor"]) ? string.Format("{0}.{1}.{2}", group.R, group.G, group.B) : args.Parameters["chatcolor"];
			var permissions = (null == args.Parameters["permissions"]) ? group.Permissions : args.Parameters["permissions"];
			try
			{
				TShock.Groups.UpdateGroup(group.Name, parent, permissions, chatcolor, group.Suffix, group.Prefix);
			}
			catch (Exception e)
			{
				return RestError(e.Message);
			}

			return RestResponse("Group '" + group.Name + "' updated successfully");
		}

		#endregion

		#region Utility Methods

		public static void DumpDescriptions()
		{
			var sb = new StringBuilder();
			var rest = new RestManager(null);

			foreach (var method in rest.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).OrderBy(f => f.Name))
			{
				if (method.IsStatic)
					continue;

				var name = method.Name;

				var descattr =
					method.GetCustomAttributes(false).FirstOrDefault(o => o is DescriptionAttribute) as DescriptionAttribute;
				var routeattr =
					method.GetCustomAttributes(false).FirstOrDefault(o => o is RouteAttribute) as RouteAttribute;

				if (descattr != null && !string.IsNullOrWhiteSpace(descattr.Description) && routeattr != null && !string.IsNullOrWhiteSpace(routeattr.Route))
				{
					sb.AppendLine("{0}  ".SFormat(name));
					sb.AppendLine("Description: {0}  ".SFormat(descattr.Description));

					var permission = method.GetCustomAttributes(false).Where(o => o is Permission);
					if (permission.Count() > 0)
					{
						sb.AppendLine("Permissions: {0}".SFormat(String.Join(", ", permission.Select(p => ((Permission)p).Name))));
					}
					else
					{
						sb.AppendLine("No special permissions are required for this route.");
					}

					var verbs = method.GetCustomAttributes(false).Where(o => o is Verb);
					if (verbs.Count() > 0)
					{
						sb.AppendLine("Verbs:");
						foreach (Verb verb in verbs)
						{
							sb.AppendLine("\t{0}({1}) [{2}] - {3}".SFormat(verb.Name, verb.Required ? "Required" : "Optional", verb.ArgumentType.Name, verb.Description));
						}
					}

					var nouns = method.GetCustomAttributes(false).Where(o => o is Noun);
					if (nouns.Count() > 0)
					{
						sb.AppendLine("Nouns:");
						foreach (Noun noun in nouns)
						{
							sb.AppendLine("\t{0}({1}) [{2}] - {3}".SFormat(noun.Name, noun.Required ? "Required" : "Optional", noun.ArgumentType.Name, noun.Description));
						}
					}
					sb.AppendLine("Example Usage: {0}?{1}".SFormat(routeattr.Route,
						string.Join("&", nouns.Select(n => String.Format("{0}={0}", ((Noun) n).Name)))));
					sb.AppendLine();
				}
			}

			File.WriteAllText("RestDescriptions.txt", sb.ToString());
		}

		private RestObject RestError(string message, string status = "400")
		{
			return new RestObject(status) {Error = message};
		}

		private RestObject RestResponse(string message, string status = "200")
		{
			return new RestObject(status) {Response = message};
		}

		private RestObject RestMissingParam(string var)
		{
			return RestError("Missing or empty " + var + " parameter");
		}

		private RestObject RestMissingParam(params string[] vars)
		{
			return RestMissingParam(string.Join(", ", vars));
		}

		private RestObject RestInvalidParam(string var)
		{
			return RestError("Missing or invalid " + var + " parameter");
		}

		private bool GetBool(string val, bool def)
		{
			bool ret;
			return bool.TryParse(val, out ret) ? ret : def;
		}

		private object PlayerFind(IParameterCollection parameters)
		{
			string name = parameters["player"];
			if (string.IsNullOrWhiteSpace(name))
				return RestMissingParam("player");

			var found = TShock.Utils.FindPlayer(name);
			switch(found.Count)
			{
				case 1:
					return found[0];
				case 0:
					return RestError("Player " + name + " was not found");
				default:
					return RestError("Player " + name + " matches " + found.Count + " players");
			}
		}

		private object UserFind(IParameterCollection parameters)
		{
			string name = parameters["user"];
			if (string.IsNullOrWhiteSpace(name))
				return RestMissingParam("user");

			User user;
			string type = parameters["type"];
			try
			{
				switch (type)
				{
					case null:
					case "name":
						type = "name";
						user = TShock.Users.GetUserByName(name);
						break;
					case "id":
						user = TShock.Users.GetUserByID(Convert.ToInt32(name));
						break;
					default:
						return RestError("Invalid Type: '" + type + "'");
				}
			}
			catch (Exception e)
			{
				return RestError(e.Message);
			}

			if (null == user)
				return RestError(String.Format("User {0} '{1}' doesn't exist", type, name));

			return user;
		}

		private object BanFind(IParameterCollection parameters)
		{
			string name = parameters["ban"];
			if (string.IsNullOrWhiteSpace(name))
				return RestMissingParam("ban");

			string type = parameters["type"];
			if (string.IsNullOrWhiteSpace(type))
				return RestMissingParam("type");

			Ban ban;
			switch (type)
			{
				case "ip":
					ban = TShock.Bans.GetBanByIp(name);
					break;
				case "name":
					ban = TShock.Bans.GetBanByName(name, GetBool(parameters["caseinsensitive"], true));
					break;
				default:
					return RestError("Invalid Type: '" + type + "'");
			}

			if (null == ban)
				return RestError("Ban " + type + " '" + name + "' doesn't exist");

			return ban;
		}

		private object GroupFind(IParameterCollection parameters)
		{
			var name = parameters["group"];
			if (string.IsNullOrWhiteSpace(name))
				return RestMissingParam("group");

			var group = TShock.Groups.GetGroupByName(name);
			if (null == group)
				return RestError("Group '" + name + "' doesn't exist");

			return group;
		}

		private Dictionary<string, object> PlayerFilter(TSPlayer tsPlayer, IParameterCollection parameters, bool viewips = false)
		{
			var player = new Dictionary<string, object>
				{
					{"nickname", tsPlayer.Name},
					{"username", tsPlayer.User == null ? "" : tsPlayer.User.Name},
					{"group", tsPlayer.Group.Name},
					{"active", tsPlayer.Active},
					{"state", tsPlayer.State},
					{"team", tsPlayer.Team},
				};

			if (viewips)
			{
				player.Add("ip", tsPlayer.IP);
			}
			foreach (IParameter filter in parameters)
			{
				if (player.ContainsKey(filter.Name) && !player[filter.Name].Equals(filter.Value))
					return null;
			}
			return player;
		}

		private object PlayerSetMute(IParameterCollection parameters, bool mute)
		{
			var ret = PlayerFind(parameters);
			if (ret is RestObject)
				return ret;

			TSPlayer player = (TSPlayer)ret;
			player.mute = mute;
			var verb = mute ? "muted" : "unmuted";
			player.SendInfoMessage("You have been remotely " + verb);
			return RestResponse("Player " + player.Name + " was " + verb);
		}

		#endregion
	}
}
