package me.despawningbone.discordbot.command.games;

import java.awt.Color;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import javax.xml.bind.DatatypeConverter;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.knowm.xchart.BitmapEncoder;
import org.knowm.xchart.XYChart;
import org.knowm.xchart.XYChartBuilder;
import org.knowm.xchart.BitmapEncoder.BitmapFormat;
import org.knowm.xchart.XYSeries.XYSeriesRenderStyle;
import org.knowm.xchart.style.Styler.LegendPosition;

import me.despawningbone.discordbot.DiscordBot;
import me.despawningbone.discordbot.command.Command;
import me.despawningbone.discordbot.command.CommandResult;
import me.despawningbone.discordbot.command.CommandResult.CommandResultType;
import me.despawningbone.discordbot.command.games.Koohii.Accuracy;
import me.despawningbone.discordbot.command.games.Koohii.DiffCalc;
import me.despawningbone.discordbot.command.games.Koohii.Map;
import me.despawningbone.discordbot.command.games.Koohii.PPv2;
import me.despawningbone.discordbot.command.games.Koohii.PPv2Parameters;
import me.despawningbone.discordbot.command.games.Koohii.MapStats;
import me.despawningbone.discordbot.command.games.Koohii.TaikoPP;
import me.despawningbone.discordbot.utils.MiscUtils;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;

public class Osu extends Command {
	
	private final static String osuAPI = DiscordBot.tokens.getProperty("osu");

	private static DecimalFormat df = new DecimalFormat("#.##");
	
	private final static HashMap<Integer, String> modes = new HashMap<>();
	static {
		modes.put(0, "osu!");
		modes.put(1, "osu!taiko");
		modes.put(2, "osu!catch");
		modes.put(3, "osu!mania");
	}
	
	public Osu() {  //TODO add back the todos to respective sub command, automate desc for subcmds, add back typing; CLOSE ALL STREAMS
		this.desc = "All the info you need with osu!";
		this.usage = "<subcommands>";
		
		//TODO add a command to parse replays?
	
		registerSubCommand("pp", Arrays.asList("map"), (channel, user, msg, words) -> {
			//OffsetDateTime timesent = OffsetDateTime.now();
			List<String> amend = new ArrayList<String>(Arrays.asList(words));
			int temp = amend.indexOf("-w");
			String uid = null;
			boolean weight = temp != -1;
			if(weight) {
				try {
					uid = amend.get(temp + 1);
					if(!uid.contains("osu.ppy.sh/u") && (uid.startsWith("http") && uid.contains("://"))) {
						uid = getPlayer(user);
					}
					amend.subList(temp, temp + 1).clear();
				} catch (IndexOutOfBoundsException e) {
					uid = getPlayer(user);
				}
			}
			String initmap;
			try {
				initmap = amend.get(0);
			} catch (IndexOutOfBoundsException e) {
				initmap = "null";
			}
			List<String> mid = new ArrayList<String>();
			if (!initmap.startsWith("https://") && !initmap.startsWith("http://")) { //check if no map input, use discord rich presence
				String details = null;
				try (Connection con = DiscordBot.db.getConnection()) {
					ResultSet rs = con.createStatement().executeQuery("SELECT game FROM users WHERE id = " + user.getId() + ";");
					if(rs.next()) {
						details = rs.getString(1).substring(rs.getString(1).indexOf("||"));
					}
					rs.close();
				} catch (SQLException e) {
					return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
				}
				channel.sendMessage("Trying to retrieve map from discord status...").queue(m -> mid.add(m.getId()));   //DONE custom status masks presence now, will not be as effective; any way to get manually?   //JDA 4 API change fixed this
				channel.sendTyping().queue();
				/*OffsetDateTime timeReceived = OffsetDateTime.now();
				long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS));
				System.out.println("Time taken: " + ms + "ms");*/
				if (details != null) {   //TODO if name is sth like `Feryquitous - (S).0ngs//---::compilation.[TQR-f3] [-[//mission:#FC.0011-excindell.defer.abferibus]-]` it breaks (but reasonable break tbh)
					//if(game.getName().equals("osu!") && game.isRich()) {
					try {
						String title = URLEncoder.encode(details.substring(details.indexOf(" - ") + 3, details.lastIndexOf("[")).trim(), "UTF-8");
						String diff = URLEncoder.encode(details.substring(details.lastIndexOf("[") + 1, details.lastIndexOf("]")).trim(), "UTF-8");
						
						String url = "https://osusearch.com/query/?title=" + title + "&diff_name=" + diff + "&query_order=play_count&offset=0";
						URLConnection stream = new URL(url).openConnection();
						stream.addRequestProperty("User-Agent", "Mozilla/4.0");
						JSONTokener tokener = new JSONTokener(stream.getInputStream());
						initmap = "https://osu.ppy.sh/beatmaps/" + new JSONObject(tokener).getJSONArray("beatmaps").getJSONObject(0).getInt("beatmap_id");
					} catch (IOException e) {
						return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
					} catch (JSONException e) {
						e.printStackTrace();
						System.out.println(details);
						return new CommandResult(CommandResultType.NORESULT);  //returns if osusearch isnt updated fast enough
					}
					
					/*SELENIUM DEPRECATED*/
					
					/*String url = "http://osusearch.com/search/?title=" + title + "&diff_name=" + diff
							+ "&query_order=play_count";
					System.out.println(url);
					//MsgListener.driver.manage().timeouts().implicitlyWait(2000, TimeUnit.MILLISECONDS);
					DiscordBot.driver.get(url);
					WebDriverWait wait = new WebDriverWait(DiscordBot.driver, 10);
					wait.until(ExpectedConditions
							.elementToBeClickable(By.cssSelector("div[class~=beatmap-list]")));
					Document document = Jsoup.parse(DiscordBot.driver.getPageSource());
					//System.out.println(document.toString());
					initmap = document.body()
							.select("div[class~=beatmap-list]")
							.first()
							.child(0)
							.select("div.truncate.beatmap-title a").get(0).attr("href");*/	
				} else {
					return new CommandResult(CommandResultType.FAILURE, "There is no account of your rich presence, therefore I cannot get the beatmap from your status.");
				}
				/*
				 * } else { throw new
				 * IllegalArgumentException("Rich presence is not an instance of osu!, therefore I cannot get the beatmap from your status."
				 * ); }
				 */
			}
			if(initmap.equals("null")) {   //shouldnt throw at all
				return new CommandResult(CommandResultType.FAILURE, "You haven't played any maps I can recognize yet!");
			}
			List<String> params = Arrays.asList(words);
			double dacc = 100;
			int combo = -1, mods = 0, miss = 0;
			PPv2Parameters p = new Koohii.PPv2Parameters();
			try {
				p.beatmap = new Koohii.Parser()
						.map(new BufferedReader(new InputStreamReader(getMap(initmap), "UTF-8")));
			} catch (IOException e1) {
				return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
			} catch (UnsupportedOperationException e1) {
				return new CommandResult(CommandResultType.FAILURE, "This gamemode is not yet supported.");
			} catch (IllegalArgumentException e1) {
				return new CommandResult(CommandResultType.INVALIDARGS, e1.getMessage());
			}
			if (p.beatmap.title.isEmpty()) {
				return new CommandResult(CommandResultType.INVALIDARGS, "Invalid beatmap.");
			}
			try {
				for (int i = 0; i < params.size(); i++) {
					String param = params.get(i);
					if (param.startsWith("+")) {
						mods = Koohii.mods_from_str(param.substring(1).toUpperCase());
					} else if (param.toLowerCase().endsWith("m")) {
						miss = Integer.parseInt(param.substring(0, param.length() - 1));
					} else if (param.endsWith("%")) {
						dacc = Double.parseDouble(param.substring(0, param.length() - 1));
					} else if (param.toLowerCase().endsWith("x")) {
						combo = Integer.parseInt(param.substring(0, param.length() - 1));
					}
				}
			} catch (NumberFormatException e) {
				return new CommandResult(CommandResultType.INVALIDARGS, "Invalid value inputted.");
			}
			DiffCalc stars = new Koohii.DiffCalc().calc(p.beatmap, mods);
			Accuracy acc = new Accuracy(dacc, p.beatmap.objects.size(), miss);
			p.n50 = acc.n50;
			p.n100 = acc.n100;
			p.n300 = acc.n300;
			p.nmiss = miss;
			p.aim_stars = stars.aim;
			p.speed_stars = stars.speed;
			p.mods = mods;
			if (combo > 0) {
				p.combo = combo;
			} else {
				p.combo = p.beatmap.max_combo();
			}
			EmbedBuilder eb = new EmbedBuilder();
			JSONTokener result = null;
			InputStream stream = null;
			URL url = null;
			try {
				String addparam = "";
				if(p.beatmap.mode == 1) {
						if((mods & 1<<8) != 0) addparam = "&mods=256";
						if((mods & 1<<6) != 0) if(addparam.isEmpty()) addparam = "&mods=64"; 
						else addparam = "";						
				}
				url = new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&b=" + initmap.substring(initmap.lastIndexOf("/") + 1).split("&")[0] + addparam);
				stream = url.openStream();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
			result = new JSONTokener(stream);
			JSONObject jbm = new JSONArray(result).getJSONObject(0);
			String setid = jbm.getString("beatmapset_id");
			double accVal, ppVal, starsVal;
			MapStats stats = new MapStats(p.beatmap);
			try {
				PPv2 pp = new PPv2(p);	
				ppVal = pp.total;
				accVal = acc.value();  //somehow real_acc aint correct, pretty sure i screwed sth up
				starsVal = stars.total;
				Koohii.mods_apply(mods, stats, 1|2|4|8);
			} catch (UnsupportedOperationException e) {  //should always only be taiko
				starsVal = jbm.getDouble("difficultyrating");
				TaikoPP pp = new TaikoPP(starsVal, dacc, p);
				ppVal = pp.pp;
				accVal = pp.acc;
				Koohii.mods_apply(mods, stats, 0);  //want nothing but speed to be changed
			}
			String totaldur = MiscUtils.convertMillis(TimeUnit.SECONDS.toMillis((int)(jbm.getInt("total_length") / stats.speed)));
			String draindur = MiscUtils.convertMillis(TimeUnit.SECONDS.toMillis((int)(jbm.getInt("hit_length") / stats.speed)));
			if(jbm.has("source") && !jbm.getString("source").isEmpty()) eb.setTitle("Source: " + jbm.getString("source"));
			eb.setDescription("Mode: " + modes.get(p.beatmap.mode));
			eb.setAuthor("PP information for " + (p.beatmap.title_unicode.isEmpty() ? p.beatmap.title : p.beatmap.title_unicode),
					initmap, "https://b.ppy.sh/thumb/" + setid + ".jpg");
			eb.addField("Artist", (p.beatmap.artist_unicode.isEmpty() ? p.beatmap.artist : p.beatmap.artist_unicode),
					true);
			eb.addField("Created by", p.beatmap.creator, true);
			eb.addField("Duration", totaldur + " | Drain " + draindur, true);
			eb.addField("Difficulty", p.beatmap.version, true);
			eb.addField("Mods", (p.mods == 0 ? "None" : Koohii.mods_str(p.mods)), true);
			eb.addField("BPM", df.format(jbm.getDouble("bpm") * stats.speed), true);
			eb.addField("Accuracy", df.format(accVal * 100), true);
			eb.addField("Combo", String.valueOf(p.combo), true);
			eb.addField("Misses", String.valueOf(p.nmiss), true);
			eb.addField("300", String.valueOf(p.n300), true);
			eb.addField("100", String.valueOf(p.n100), true);
			eb.addField("50", String.valueOf(p.n50), true);
			eb.addField("PP", df.format(ppVal), true);
			if(weight) {
				try {
					String gain = df.format(weightForPP(ppVal, jbm.getString("beatmap_id"), getUserBest(uid, p.beatmap.mode)));
					eb.addField("Actual pp gain for " + uid, (gain.equals("-1") ? "User has a better score than this" : gain + "pp"), true);
				} catch (IllegalArgumentException e1) {
					return new CommandResult(CommandResultType.INVALIDARGS, e1.getMessage());
				}
			}
			eb.setFooter("Stars: " + df.format(starsVal) + " | CS: " + df.format(stats.cs) + " | HP: " + df.format(stats.hp)
					+ " | AR: " + df.format(stats.ar) + " | OD: " + df.format(stats.od), null);
			//e.setColor(new Color(248, 124, 248));
			eb.setColor(new Color(239, 109, 167));
			if(!mid.isEmpty()) {
				channel.deleteMessageById(mid.get(0)).queue();
			}
			//channel.sendMessage(e.build()).queue();
			channel.sendMessage(eb.build()).queue();
			return new CommandResult(CommandResultType.SUCCESS);
		}, "[beatmap URL] [+|%|x|m] [-w [username]]",  Arrays.asList("-w", "+DT", "https://osu.ppy.sh/b/1817768 93.43% -w", "https://osu.ppy.sh/beatmaps/1154509 +HDDT 96.05% 147x 2m -w despawningbone"),
				"Check PP information about that beatmap!", Arrays.asList(
				"  * Available parameters: +[mod alias], [accuracy]%, [combo]x, [misses]m",
				"  * specify the `-w` parameter with a username (or none for your own) to check how many actual pp the play will get them!",
				"  * You can also not input the URL, if you are currently playing osu! and discord sensed it."));
		
		
		registerSubCommand("weight", Arrays.asList("w"), (channel, user, msg, words) -> {
			//System.out.println(weightForPP(95.91, null, null));
			//String debugMax = "", debugMin = "", debugData = "", debugAll = "";
			List<String> params = new ArrayList<>(Arrays.asList(words));
			int modeId = getMode(params);
			final double precision = 1e-4, iterations = 500;
			String uid;
			int index = params.size() < 2 ? 0 : 1;
			try {
				uid = params.get(index - 1);
				if(index == 0 || (!uid.contains("osu.ppy.sh/u") && (uid.startsWith("http") && uid.contains("://")))) {
					uid = getPlayer(user);
				}
			} catch (IndexOutOfBoundsException e) {
				uid = getPlayer(user);
			}
			JSONArray arr = null;
			try {
				arr = getUserBest(uid, modeId);
			} catch (IllegalArgumentException e) {
				return new CommandResult(CommandResultType.INVALIDARGS, e.getMessage());
			}
			double maxPP = arr.getJSONObject(0).getDouble("pp") + 100;  //fricc those who wanna get pp over 100pp more than they can lmao
			try {
				//double basePP = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/get_user?k=" + osuAPI + "&u=" + uid).openStream())).getJSONObject(0).getDouble("pp_raw");
				double targetPP = params.size() > index ? Double.parseDouble(params.get(index)) : 1;
				int i = 0; double minPP = 0, mid = 0;
				/*for(i = 0; i < 600; i++) {
					//debugData += weightForPP(i, null, arr) + "\n";
					debugData += i + " " + weightForPP(i, null, arr) + "\n";
				}*/
				//System.out.println(debugData);
				i = 0;
				while(i < iterations && Math.abs(maxPP - minPP) > precision) {
					mid = (maxPP + minPP) / 2;
					double temp = weightForPP(mid, null, arr);
					//System.out.println(i + ", " + mid + ", " + temp);
					if(temp > targetPP) {
						maxPP = mid;
					} else {
						minPP = mid;
					}
					i++;
					//System.out.println(maxPP + " " + minPP);
					//debugAll += maxPP + " " + minPP + "\n";
					//debugMax += maxPP + "";
					//debugMin += minPP + " ";
				}
				//System.out.println(debugAll);
				//System.out.println(debugMax);
				//System.out.println(debugMin);
				if(!df.format(mid).equals(df.format(arr.getJSONObject(0).getDouble("pp") + 100))) {
					channel.sendMessage("For " + uid + " to actually gain " + df.format(targetPP) + "pp in " + modes.get(modeId) + ", they have to play a map worth approximately **" + df.format(mid) + "pp** raw.").queue();
					return new CommandResult(CommandResultType.SUCCESS);
				} else {
					return new CommandResult(CommandResultType.INVALIDARGS, "You can't really achieve such large pp jumps without people thinking you are hacking :P");
				}
			} catch (NumberFormatException e) {
				return new CommandResult(CommandResultType.INVALIDARGS, "Invalid target pp specified.");
			}
			//return "";
		}, "[username] [wished pp gain]", Arrays.asList("", "10", "despawningbone 20"),
				"Estimate how much raw pp a map needs to have in order to gain you pp!",
				Arrays.asList("  * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard)."));
		
		
		//TODO NEED EXTREME TIDYING UP
		//TODO merge parts of the code with getRecent() and getPP();
		registerSubCommand("recent", Arrays.asList("r"), (channel, user, msg, words) -> {
			List<String> params = new ArrayList<>(Arrays.asList(words));
			int modeId = getMode(params);
			if(modeId > 1) modeId = 0;
			boolean passOnly = params.removeAll(Collections.singleton("-p"));  //no need space, as spaced ones are split (if a player has a name with spaced -p it might be a problem)
			String[] split = String.join(" ", params).split("\\|");
			String search = split[0].trim();
			int nrecent = 0;
			try {
				nrecent = Integer.parseInt(split[1].trim()) - 1;
			} catch (NumberFormatException e) {
				//print to channel?
			} catch (ArrayIndexOutOfBoundsException e) {
				;  //nothing special about this
			}
			if(nrecent > 50) {
				return new CommandResult(CommandResultType.FAILURE, "Only the 50 most recent plays can be recalled!");
			}
			String id = "";
			boolean noturl = false;
			try {
				new URL(search);
				id = search.substring(search.lastIndexOf("/") + 1);
			} catch (MalformedURLException e) {
				noturl = true;
				id = search;
			}
			if(id.isEmpty()) {
				id = getPlayer(user);
			}
			JSONTokener result = null;
			InputStream stream = null;
			URL url = null;
			try {
				String addparam = "";
				if (noturl) {
					addparam = "&type=string";
				}
				if (modeId != 0) {
					addparam += ("&m=" + modeId);
				}
				url = new URL(
						"https://osu.ppy.sh/api/get_user_recent?k=" + osuAPI + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam + "&limit=50");
				stream = url.openStream();
				if (stream.available() < 4) {
					return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + id + "` or the player has not been playing in the last 24h.");
				}
			} catch (IOException ex) {
				ex.printStackTrace();
				return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex));
			}
			result = new JSONTokener(stream);
			JSONArray array = new JSONArray(result);
			//System.out.println(array);
			if(array.length() > 0) {
				JSONObject mostRecent = null;
				if(passOnly) {
					int times = 0;
					for(int i = 0; i < array.length(); i++) {
						if(!array.getJSONObject(i).getString("rank").equals("F")) {
							times++;
							if(times - 1 >= nrecent) {
								mostRecent = array.getJSONObject(i);
								nrecent = i;
								break;
							}
						}
					}
					if(mostRecent == null) {
						return new CommandResult(CommandResultType.INVALIDARGS, "You did not have this much passed plays in the recent 50 plays!");
					}
				} else {
					try {
						mostRecent = array.getJSONObject(nrecent);					
					} catch (JSONException e) {
						return new CommandResult(CommandResultType.FAILURE, "You only played " + array.length() + " times recently!");
					}
				}
				String beatmap = mostRecent.getString("beatmap_id");
				String name;
				if (noturl) {
					name = id;
					id = mostRecent.getString("user_id");
				} else {
					try {
						Long.parseLong(id);
						//get more info from here?
						name = new JSONObject(new JSONTokener(new URL("https://osu.ppy.sh/api/get_user?k=" + osuAPI + "&u=" + id).openStream())).getString("username");
					} catch (NumberFormatException e) {
						name = id;
					} catch (IOException e) {
						return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
					}
				}
				int i;
				String more = "";
				for(i = nrecent + 1; i < array.length(); i++) {
					if(!array.getJSONObject(i).getString("beatmap_id").equals(beatmap)) {
						break;
					}
					if(i == array.length() - 1) {
						more = " (or more)";
					}
				}
				if(i == array.length()) {
					more = " (or more)";
				}
				PPv2Parameters p = new Koohii.PPv2Parameters();
				try {
					p.beatmap = new Koohii.Parser()
							.map(new BufferedReader(new InputStreamReader(getMap(beatmap), "UTF-8")));
				} catch (IOException e1) {
					return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
				} catch (UnsupportedOperationException e1) {
					return new CommandResult(CommandResultType.FAILURE, "This gamemode is not yet supported.");
				} catch (IllegalArgumentException e1) {
					return new CommandResult(CommandResultType.INVALIDARGS, e1.getMessage());
				}
				int mods = mostRecent.getInt("enabled_mods");
				if (p.beatmap.title.isEmpty()) {
					return new CommandResult(CommandResultType.INVALIDARGS, "Invalid beatmap.");
				}
				DiffCalc stars = new Koohii.DiffCalc().calc(p.beatmap, mods);
				p.n50 = mostRecent.getInt("count50");
				p.n100 = mostRecent.getInt("count100");
				p.n300 = mostRecent.getInt("count300");
				p.nmiss = mostRecent.getInt("countmiss");
				//System.out.println(p.n300);
				p.nobjects = p.n300 + p.n100 + p.n50 + p.nmiss;
				//System.out.println(p.nobjects);
				p.aim_stars = stars.aim;
				p.speed_stars = stars.speed;
				p.mods = mods;
				p.combo = mostRecent.getInt("maxcombo");
				EmbedBuilder eb = new EmbedBuilder();
				//System.out.println(mods);
				String modStr = Koohii.mods_str(mods);
				eb.setColor(new Color(0, 0, 0));
				double ppVal, ppMax, starVal, accVal;
				JSONObject obj;
				try {
					String addparam = "";
					if(p.beatmap.mode == 1) {
							if((mods & 1<<8) != 0) addparam = "&mods=256";
							if((mods & 1<<6) != 0) if(addparam.isEmpty()) addparam = "&mods=64"; 
							else addparam = "";						
					}
					obj = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&b=" + mostRecent.getInt("beatmap_id") + addparam).openStream())).getJSONObject(0);
				} catch (JSONException | IOException e) {
					return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
				}
				try {
					PPv2 pp = new PPv2(p);	
					starVal = stars.total;
					accVal = pp.computed_accuracy.value();
					ppVal = pp.total;
					ppMax = constructPP(stars.aim, stars.speed, p.beatmap, mods).total;
				} catch (UnsupportedOperationException e) {
					starVal = obj.getDouble("difficultyrating");
					TaikoPP pp = new TaikoPP(starVal, p);
					ppVal = pp.pp; 
					accVal = pp.acc;
					ppMax = new TaikoPP(starVal, p.max_combo, mods, 1, 0, p.beatmap).pp;
				}
				eb.setTitle((p.beatmap.artist_unicode.isEmpty() ? p.beatmap.artist : p.beatmap.artist_unicode) + " - " + (p.beatmap.title_unicode.isEmpty() ? p.beatmap.title : p.beatmap.title_unicode) + " [" + p.beatmap.version + "]" + (modStr.isEmpty() ? "" : " +" + modStr) + " (" + df.format(starVal) + "*)", "https://osu.ppy.sh/beatmaps/" + beatmap);
				eb.setAuthor(MiscUtils.ordinal(nrecent + 1) + " most recent " + modes.get(modeId) + " play by " + name, "https://osu.ppy.sh/users/" + id, "https://a.ppy.sh/" + id);
				eb.setDescription(MiscUtils.ordinal(i - nrecent) + more + " consecutive try\nRanking: " + mostRecent.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + (mostRecent.getString("rank").equals("F") ? " (Estimated completion percentage: " +  df.format(p.nobjects * 100.0 / p.beatmap.objects.size()) + "%)" : ""));
				eb.setThumbnail("https://b.ppy.sh/thumb/" + obj.getString("beatmapset_id") + ".jpg");	
				eb.addField("Score", String.valueOf(mostRecent.getInt("score")), true);
				eb.addField("Accuracy", df.format(accVal * 100) + " (" + p.n300 + "/" + p.n100 + "/" + p.n50 + "/" + p.nmiss + ")", true);
				eb.addField("Combo", (mostRecent.getInt("perfect") == 0 ? p.combo + "x / " + p.beatmap.max_combo() + "x" : "FC (" + p.combo + "x)"), true);
				eb.addField("PP", df.format(ppVal) + "pp / " + df.format(ppMax) + "pp", true);
				eb.setFooter("Score submitted", null);
				eb.setTimestamp(OffsetDateTime.parse(mostRecent.getString("date").replace(" ", "T") + "+00:00"));
				channel.sendMessage(eb.build()).queue();
				return new CommandResult(CommandResultType.SUCCESS);
			} else {
				return new CommandResult(CommandResultType.FAILURE, "You have no recent plays in this 24h!");
			}
		}, "[-p] [username] [| index]", Arrays.asList("", "-p | 2", "-p despawningbone"),
				"Check a user's recent play!", Arrays.asList(
				"  * Specifying the `-p` parameter will only search for plays that did not fail.",
				"  * Supports `-t` for taiko (Defaults to standard)."));
		
		
		//TODO tidy up first part, merge with getTopPlays()?
		registerSubCommand("ppchart", Arrays.asList("ppc"), (channel, user, msg, words) -> {
			if(words.length < 30) {
				List<String> params = new ArrayList<>(Arrays.asList(words));
				int modeId = getMode(params);
				if(params.size() < 1) {
					params.add(getPlayer(user));
				}
				String[] split = String.join(" ", params).split("\\|");
				String concName = String.join(" | ", split);
				XYChart chart = new XYChartBuilder().width(800).height(600).title("Top PP plays for " + concName + " (" + modes.get(modeId) + ")").yAxisTitle("PP").xAxisTitle("Plays (100 = top)").build();
				chart.getStyler().setDefaultSeriesRenderStyle(XYSeriesRenderStyle.Scatter);
				chart.getStyler().setLegendPosition(LegendPosition.InsideSE);
				chart.getStyler().setPlotContentSize(0.91);
				chart.getStyler().setMarkerSize(7);
				//chart.getStyler().setAxisTitlePadding(24);
				chart.getStyler().setChartTitlePadding(12);
				chart.getStyler().setChartPadding(12);
				chart.getStyler().setChartBackgroundColor(new Color(200, 220, 230));
				//ThreadLocalRandom ran = ThreadLocalRandom.current();
				for(String name : split) {
					List<Double> pps = new ArrayList<>();
					try {
						JSONArray array = getUserBest(name.trim(), modeId);
						for(int i = 0; i < array.length(); i++) {
							JSONObject obj = array.getJSONObject(i);
							pps.add(obj.getDouble("pp"));
						}
					} catch (IllegalArgumentException e) {
						return new CommandResult(CommandResultType.INVALIDARGS, e.getMessage());
					}
					Collections.reverse(pps);
					/*XYSeries series =*/ chart.addSeries(name.trim() + "'s PP", pps);
					//use series.setXYSeriesRenderStyle() for average line
					//series.setMarkerColor(new Color(ran.nextInt(1, 255), ran.nextInt(1, 255), ran.nextInt(1, 255)));
				}
				ByteArrayOutputStream os = new ByteArrayOutputStream();
				try {
					BitmapEncoder.saveBitmap(chart, os, BitmapFormat.PNG);
				} catch (IOException e) {
					return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
				}
				channel.sendFile(new ByteArrayInputStream(os.toByteArray()), "ppchart.png").queue();
				return new CommandResult(CommandResultType.SUCCESS);
			} else {
				return new CommandResult(CommandResultType.INVALIDARGS, "Please do not include so many players at once.");
			}
		}, "[mode] [username] [| usernames]...", Arrays.asList("", "taiko", "FlyingTuna | Rafis | despawningbone"),
				"Get a graph with the users' top plays!", Arrays.asList(
				"  * You can specify up to 30 players at once, seperated by `|`."
				+ "  * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard)."));
		
		
		registerSubCommand("set", Arrays.asList("s"), (channel, user, msg, words) -> {
			int page = 1;
			if (words.length < 1) {
				return new CommandResult(CommandResultType.INVALIDARGS, "Please enter search words or an URL.");
			}/* else if (words.length > 2) {
				try {
					page = Integer.parseInt(words[2]);
				} catch (NumberFormatException e) {
					throw new IllegalArgumentException("Please enter a valid page number.");
				}
			}*/
			String id = "", set = "";
			try {
				new URL(words[0]);
			} catch (MalformedURLException e) {
				//throw new IllgalArgumentException("Invalid URL."); //NOFIX: for some reason, discord seems to continue the typing after this has been sent  //its because of queue()
				try {
					String[] split = String.join(" ", words).split("\\|");
					String search = URLEncoder.encode(split[0].trim(), "UTF-8");
					try {
						if(split.length > 1) page = Integer.parseInt(split[1].trim());
					} catch (NumberFormatException e1) {
						return new CommandResult(CommandResultType.INVALIDARGS, "Please enter a valid page number.");
					}
					String url = "https://osusearch.com/query/?title=" + search + "&query_order=play_count&offset=0";
					URLConnection stream;
					stream = new URL(url).openConnection();
					stream.addRequestProperty("User-Agent", "Mozilla/4.0");
					JSONTokener tokener = new JSONTokener(stream.getInputStream());
					id = String.valueOf(new JSONObject(tokener).getJSONArray("beatmaps").getJSONObject(0).getInt("beatmapset_id"));
					set = "https://osu.ppy.sh/beatmapset/" + id;
				} catch (IOException | JSONException e1) {
					if(e1 instanceof JSONException) {
						return new CommandResult(CommandResultType.NORESULT);
					} else {
						return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
					}
				}
			}
			if(id.equals("")) {
				set = words[0];
				id = set.substring(set.lastIndexOf("/") + 1);
				if (set.contains("/beatmapsets/")) {
					id = set.substring(set.lastIndexOf("/beatmapsets/"));
					id = id.substring(id.lastIndexOf("/") + 1);
				} else {
					if (set.contains("/b/") || set.contains("#")) {
						return new CommandResult(CommandResultType.INVALIDARGS, "This is a specific beatmap, not a beatmap set.");
					}
					id = id.split("&")[0];
				}	
			}
			JSONTokener result = null;
			InputStream stream = null;
			URL url = null;
			try {
				url = new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&s=" + id);
				stream = url.openStream();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
			result = new JSONTokener(stream);
			JSONArray arr = new JSONArray(result);
			List<JSONObject> obj = new ArrayList<JSONObject>();
			for (int i = 0; i < arr.length(); i++) {
				obj.add(arr.getJSONObject(i));
			}
			List<JSONObject> fobj = obj.stream().sorted(Comparator.comparing(e -> e.getDouble("difficultyrating")))
					.collect(Collectors.groupingBy(e -> e.getInt("mode"))).values().stream().flatMap(List::stream)
					.collect(Collectors.toList());
			EmbedBuilder em = new EmbedBuilder();
			JSONObject fo;
			try {
				fo = fobj.get(0);
			} catch (IndexOutOfBoundsException e) {
				return new CommandResult(CommandResultType.INVALIDARGS, "Invalid beatmap URL.");
			}
			if(page > Math.ceil((double) fobj.size() / 3)) {
				return new CommandResult(CommandResultType.INVALIDARGS, "The beatmap set does not have this many difficulties!");
			}
			em.setAuthor("Beatmap set information of " + fo.getString("title"), set,
					"https://b.ppy.sh/thumb/" + id + ".jpg");
			em.setDescription("Author: " + fo.getString("artist"));
			em.addField("BPM", fo.getString("bpm"), true);
			em.addField("Creator", fo.getString("creator"), true);
			em.addField("Approved/Ranked", (fo.getInt("approved") >= 1 ? "Yes" : "No"), true);
			em.addBlankField(false);
			int n = ((page - 1) * 3);
			//String desc = "";
			for (int i = n; (n + 3 <= fobj.size() ? i < n + 3 : i < fobj.size()); i++) {
				JSONObject json = fobj.get(i);
				String mode = modes.get(json.getInt("mode"));
				/*desc += "**[" + json.getString("version") + "](https://osu.ppy.sh/b/" + json.getString("beatmap_id") + ")** (" +  mode  + ")\n";
				desc += df.format(json.getDouble("difficultyrating")) + "* " + (json.get("max_combo").toString() + "*").replaceAll("null*", "N/A") + "";*/
				em.addField("Difficulty", "[" + json.getString("version") + "](https://osu.ppy.sh/b/" + json.getString("beatmap_id") + ")" + " ([Preview](http://bloodcat.com/osu/preview.html#" + json.getString("beatmap_id") + "))", false);
				/*em.addField("URL", "https://osu.ppy.sh/b/" + json.getString("beatmap_id"), true);
				em.addBlankField(true);*/
				em.addField("Max combo", json.get("max_combo").toString().replaceAll("null", "N/A"), true);
				em.addField("Stars", df.format(json.getDouble("difficultyrating")), true);
				em.addField("Mode", mode, true);
			}
			em.setFooter("Page: " + String.valueOf(page) + "/" + String.valueOf((int) Math.ceil((double) fobj.size() / 3)),
					null);
			channel.sendMessage(em.build()).queue();
			return new CommandResult(CommandResultType.SUCCESS);
		}, "<beatmap set URL/search words> [| page]\n", Arrays.asList("big black", "high free spirits | 2"),
				"Check info about a beatmap set!", Arrays.asList(
				"  * Useful to get the specific difficulty URL for !desp osu pp."));
		
		//TODO add userset for setting username to db so getPlayer wont be wrong?
		registerSubCommand("user", Arrays.asList("u"), (channel, user, msg, words) -> {
			List<String> params = new ArrayList<>(Arrays.asList(words));
			int modeId = getMode(params);
			String search = String.join(" ", params);
			String id = "";
			boolean noturl = false;
			try {
				new URL(search);
				id = search.substring(search.lastIndexOf("/") + 1);
			} catch (MalformedURLException e) {
				noturl = true;
				id = search;
			}
			if(id.isEmpty()) {
				id = getPlayer(user);
			}
			JSONTokener result = null;
			InputStream stream = null;
			URL url = null;
			try {
				String addparam = "";
				if (noturl) {
					addparam = "&type=string";
				}
				if (modeId != 0) {
					addparam += ("&m=" + modeId);
				}
				url = new URL(
						"https://osu.ppy.sh/api/get_user?k=" + osuAPI + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam);
				stream = url.openStream();
				if (stream.available() < 4) {
					return new CommandResult(CommandResultType.INVALIDARGS, "Unknown user `" + id + "`.");
				}
			} catch (IOException ex) {
				ex.printStackTrace();
			}
			result = new JSONTokener(stream);
			JSONObject usr = new JSONArray(result).getJSONObject(0);
			
			id = usr.getString("user_id");  //no matter if its url or not user_id is still gonna be the most accurate one
			
			EmbedBuilder em = new EmbedBuilder();
			em.setAuthor("User info of " + usr.getString("username") + " (" + modes.get(modeId) + ")", "https://osu.ppy.sh/users/" + id);
			String img = "https://a.ppy.sh/" + id;
			try {
				new URL(img).openStream();
			} catch (IOException e) {
				img = "https://s.ppy.sh/images/blank.jpg";
			}
			em.setThumbnail(img);
			try {
				em.addField("PP", usr.getDouble("pp_raw") == 0 ? "No Recent Plays" : usr.getString("pp_raw"), true);
				em.addField("Accuracy", df.format(usr.getDouble("accuracy")), true);
			} catch (JSONException e) {
				return new CommandResult(CommandResultType.FAILURE, "This user has not played any map in " + modes.get(modeId) + " before.");
			}
			StringBuffer unibuff = new StringBuffer();
			char[] ch = usr.getString("country").toLowerCase().toCharArray();
			for (char c : ch) {
				int temp = (int) c;
				int temp_integer = 96; //for lower case
				if (temp <= 122 & temp >= 97)
					unibuff.append(Character.toChars(127461 + (temp - temp_integer)));
			}
			em.addField("Rank", (usr.getInt("pp_rank") == 0 ? "N/A" : "#" + usr.getString("pp_rank")) + " | Country #" + usr.getString("pp_country_rank"), true);
			em.addField("Country", unibuff.toString(), true);
			em.addField("Level", df.format(usr.getDouble("level")), true);
			em.addField("Play count", usr.getString("playcount"), true);
			em.addField("SS+", usr.getString("count_rank_ssh"), true);
			em.addField("SS", usr.getString("count_rank_ss"), true);
			double total = usr.getDouble("total_score");
			em.addField("Total score", String.valueOf((long) total), true);
			em.addField("S+", usr.getString("count_rank_sh"), true);
			em.addField("S", usr.getString("count_rank_s"), true);
			double ranked = usr.getDouble("ranked_score");
			em.addField("Ranked score", String.valueOf((long) ranked) + " (" + df.format((ranked / total) * 100) + "%)", true);
			em.setColor(new Color(66, 134, 244));
			em.setFooter("300: " + usr.getString("count300") + " | 100: " + usr.getString("count100") + " | 50: "
					+ usr.getString("count50"), null); //TODO display percentage?
			channel.sendMessage(em.build()).queue();
			return new CommandResult(CommandResultType.SUCCESS);
		}, "[gamemode] [user URL|keyword]", Arrays.asList("", "despawningbone", "taiko despawningbone"),
				"Check info about a user!", Arrays.asList(
				"  * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard)."));
		

		//DONE api demanding; need to add cooldown  //nvm i changed it to use XHR now
		//parts mergeable with getRecent() 
		registerSubCommand("topplays", Arrays.asList("top", "t"), (channel, user, msg, words) -> {
			List<String> params = new ArrayList<>(Arrays.asList(words));
			int modeId = getMode(params), page;
			String[] split = String.join(" ", params).split("\\|");
			String search = split[0];
			try {
				page = Integer.parseInt(split[1].trim());
				if(page > 10) {
					return new CommandResult(CommandResultType.INVALIDARGS, "You can only request your top 100 plays!");
				}
			} catch (NumberFormatException e) {
				return new CommandResult(CommandResultType.INVALIDARGS, "Invalid page number!");
			} catch (ArrayIndexOutOfBoundsException e) {
				page = 1;  //its normal for people to not input index
			}
			String id = "";
			boolean noturl = false;
			try {
				new URL(search);
				id = search.substring(search.lastIndexOf("/") + 1);
			} catch (MalformedURLException e) {
				noturl = true;
				id = search;
			}
			if(id.isEmpty()) {
				id = getPlayer(user);
			}
			JSONTokener result = null;
			InputStream stream = null;
			String name, addparam = "";
			try {
				if (noturl) {
					addparam = "&type=string";
				}
				JSONObject userObj = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/get_user?k=" + osuAPI + "&u=" + id + addparam).openStream())).getJSONObject(0);
				name = userObj.getString("username");
				id = userObj.getString("user_id");
				URLConnection con = new URL("https://osu.ppy.sh/users/" + id + "/scores/best?mode=" + (modeId == 0 ? "osu" : modeId == 1 ? "taiko" : modeId == 2 ? "fruits" : modeId == 3 ? "mania" : "osu") + "&offset=" + (page - 1) * 10 + "&limit=10").openConnection();
				stream = con.getInputStream();
				if (stream.available() < 4) {
					return new CommandResult(CommandResultType.FAILURE, "This player does not have this many plays in this gamemode!");
				}
			} catch (IOException ex) {
				return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex));
			} catch (JSONException ex) {
				return new CommandResult(CommandResultType.INVALIDARGS, "Unknown user `" + id + "`.");
			}
			result = new JSONTokener(stream);
			JSONArray array = new JSONArray(result);
			EmbedBuilder eb = new EmbedBuilder();
			eb.setAuthor(name + "'s top plays (" + modes.get(modeId) + ")", "https://osu.ppy.sh/users/" + id, "https://a.ppy.sh/" + id);
			DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
			try {
				for(int i = 0; i < array.length(); i++) {
					JSONObject obj = array.getJSONObject(i);
					String mods = obj.getJSONArray("mods").join("").replaceAll("\"", "");
					/*bInfo = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&b=" + bId).openStream())).getJSONObject(0);
					String info = "**" + obj.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + "** "
							+ df.format(bInfo.getDouble("difficultyrating")) + "*"
							+ (!mods.isEmpty() ? " **" + mods + "** " : " ")
							+ " ([link](https://osu.ppy.sh/beatmap/" + bId + "))"
							+ "\n" + (obj.getDouble("pp") + "pp (" + obj.getInt("count300") + "/" + obj.getInt("count100") + "/" + obj.getInt("count50") + "/" + obj.getInt("countmiss") + ")")
							+ " "  + (obj.getInt("perfect") == 1 ? "FC " + obj.getInt("maxcombo") + "x" : obj.getInt("maxcombo") + (!bInfo.isNull("max_combo") ? "/" + bInfo.getInt("max_combo") + "x" : "x")) 
							+ "\nPlayed on " + obj.getString("date");
					eb.addField((i + 1) + ". " + bInfo.getString("artist") + " - " + bInfo.getString("title") + " [" + bInfo.getString("version") + "]", info, false);  //no unicode unfortunately*/
					JSONObject stats = obj.getJSONObject("statistics");
					JSONObject bInfo = obj.getJSONObject("beatmap");
					JSONObject bsInfo = obj.getJSONObject("beatmapset");
					int bId = bInfo.getInt("id");
					String info = "**" + obj.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + "**"
							+ (!mods.isEmpty() ? " **" + mods + "** " : " ")
							+ df.format(bInfo.getDouble("difficulty_rating")) + "*"
							+ " ([map link](https://osu.ppy.sh/beatmaps/" + bId + "))"
							+ "\nScore: " + obj.getInt("score")
							+ "\n**" + (obj.getDouble("pp") + "pp** | Weighted " + df.format(obj.getJSONObject("weight").getDouble("pp")) + "pp (" + df.format(obj.getJSONObject("weight").getDouble("percentage")) + "%)"
							+ "\n" + df.format(obj.getDouble("accuracy") * 100) + "% " + (obj.getBoolean("perfect") ? "FC " + obj.getInt("max_combo") + "x" : obj.getInt("max_combo") + "x") + " (" + stats.getInt("count_300") + "/" + stats.getInt("count_100") + "/" + stats.getInt("count_50") + "/" + stats.getInt("count_miss") + ")") 
							+ "\nPlayed on " + format.format(DatatypeConverter.parseDateTime(obj.getString("created_at")).getTime());
					eb.addField(((page - 1) * 10 + (i + 1)) + ". " + bsInfo.getString("artist") + " - " + bsInfo.getString("title") + " [" + bInfo.getString("version") + "]", info, false);  //no unicode unfortunately*/
				}
				eb.setFooter("Page: " + page, null);
				eb.setColor(new Color(5, 255, 162));
			} catch (JSONException e1) {
				return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
			}
			channel.sendMessage(eb.build()).queue();
			return new CommandResult(CommandResultType.SUCCESS);
		}, "[username] [|page]", Arrays.asList("", "| 2", "despawningbone", "-m despawningbone"),
				"Get the top plays of a user!", Arrays.asList(
				"  * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard)."));
		
		
		/*registerSubCommand("search", Arrays.asList("s"), (channel, user, msg, words) -> { //need to deprecate google search  //no longer works at all
			String search = String.join(" ", Arrays.asList(words));
			List<Entry<String, String>> url = new ArrayList<>();
			try {
				url = GoogleSearch
						.search(URLEncoder.encode("site:https://osu.ppy.sh/ " + search, "UTF-8"), 10);
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			if (!url.isEmpty()) {
				String list = "";
				Stream<Entry<String, String>> stream = url.stream();
				if (words[0].equals("mapsearch")) {
					stream = stream.filter(n -> n.getValue().contains("/b/") || n.getValue().contains("/s/")
							|| n.getValue().contains("/beatmapsets/"));
				} else if (words[0].equals("usersearch")) {
					stream = stream.filter(n -> n.getValue().contains("/u/") || n.getValue().contains("/users/"));
				}
				list = "Search results for " + search + ":\n\n";
				list += stream
						.map(e -> (e.getKey() + "\n" + e.getValue() + "\n").replaceAll("<b>", "").replaceAll("</b>", ""))
						.collect(Collectors.joining("\n"));
				if (!list.isEmpty()) {
					channel.sendMessage(list).queue();
					return new CommandResult(CommandResultType.SUCCESS);
				} else {
					return new CommandResult(CommandResultType.NORESULT);
				}
			} else {
				return new CommandResult(CommandResultType.NORESULT);
			}
		}, "search|usersearch|mapsearch [keywords]", Arrays.asList("usersearch cookiezi", "search big black"),
				"Search the osu website!", null);*/
		
		registerSubCommand("compare", Arrays.asList("c", "comp"), (channel, user, msg, words) -> {
			String map = "", name = "", img = "", mode = "";
			int index = 1;
			String[] param = String.join(" ", words).split("\\|");
			try {
				if(param.length > 1) index = Integer.parseInt(param[1].trim());	
			} catch(NumberFormatException e) {
				channel.sendMessage("Invalid index specified. Defaulting to the most recent scorecard...").queue();
			}
			for(Message card : channel.getHistory().retrievePast(100).complete()) {
				List<MessageEmbed> embeds = card.getEmbeds();
				if(embeds.size() > 0) {
					MessageEmbed embed = embeds.get(0);
					if(embed.getAuthor() == null) continue;
					String author = embed.getAuthor().getName();
					if(card.getAuthor().getId().equals(DiscordBot.BotID)) {
						if(author.contains("most recent osu!")) {
							map = embed.getUrl();
							name = embed.getTitle().lastIndexOf("+") == -1 ? embed.getTitle().substring(0, embed.getTitle().lastIndexOf("(")) : embed.getTitle().substring(0, embed.getTitle().lastIndexOf("+"));
							img = embed.getThumbnail().getUrl();
							mode = author.split("most recent osu!")[1].split(" play")[0];
						} else if(author.startsWith("PP information for")) {
							map = embed.getAuthor().getUrl();
							name = author.split("PP information for ")[1] + " [" +embed.getFields().get(3).getValue() + "]";
							img = embed.getAuthor().getIconUrl();
							mode = embed.getDescription().replace("Mode: osu!", "");
						}
					} else if(card.getAuthor().getId().equals("289066747443675143")) {  //owobot support
						if(author.startsWith("New") && author.contains("in osu!")) {
							String markdown = embed.getDescription().substring(7, embed.getDescription().indexOf("\n")).trim();
							map = markdown.substring(markdown.lastIndexOf("(") + 1, markdown.length() - 2);
							name = markdown.substring(0, markdown.indexOf("__**]"));
							img = embed.getThumbnail().getUrl();
							mode = author.substring(author.lastIndexOf("in osu! "), author.length()).trim();
						} else if(card.getContentDisplay().contains("Most Recent ")) {
							map = embed.getAuthor().getUrl();
							name = author.substring(0, author.lastIndexOf("+"));
							img = embed.getThumbnail().getUrl();
							mode = card.getContentDisplay().contains("Mania") ? "mania" : card.getContentDisplay().split("Most Recent ")[1].split(" ")[0];  //ye fucking blame owobot for being so inconsistent
						} else if(card.getContentDisplay().contains("map(s).")) {
							map = embed.getAuthor().getUrl();
							map = map.substring(0, map.length() - (map.endsWith("/") ? 1 : 0));   //fucking hell the url actually changes according to how ppl trigger it
							name = author.split(" – ")[1];
							String[] split = embed.getFields().get(0).getName().split("__", 3);
							name = name.substring(0, name.lastIndexOf(" by ")) + " [" + split[1] + "]";
							img = "https://b.ppy.sh/thumb/" + embed.getDescription().split("\\[map\\]\\(https:\\/\\/osu\\.ppy\\.sh\\/d\\/", 2)[1].split("\\)", 2)[0] + ".jpg";
							mode = split[2].isEmpty() ? "std" : split[2].substring(2, split[2].lastIndexOf("]") + 1);
						}
					}
					if(index > 1) {
						index--;
						map = "";
					}
					if(!map.isEmpty()) break;
				}
			}
			int modeId = 0;
			if(mode.equalsIgnoreCase("taiko")) modeId = 1;
			else if(mode.equalsIgnoreCase("catch") || mode.equals("ctb")) modeId = 2;
			else if(mode.equalsIgnoreCase("mania")) modeId = 3;
			if(map.isEmpty()) return new CommandResult(CommandResultType.FAILURE, "There are no recent map/score cards to compare with in the past 100 messages!");
			String uid = param[0].isEmpty() ? getPlayer(user) : param[0].replaceAll(" ", "%20");
			try {
				URL url = new URL("https://osu.ppy.sh/api/get_scores?k=" + osuAPI + "&b=" + map.substring(map.lastIndexOf("/") + 1) + "&u=" + uid + "&m=" + modeId); 
				JSONArray arr = new JSONArray(new JSONTokener(url.openStream()));
				EmbedBuilder eb = new EmbedBuilder();
				eb.setTitle("Top plays for " + arr.getJSONObject(0).getString("username") + " on " + name);
				eb.setColor(new Color(237, 154, 70));
				eb.setThumbnail(img);
				for(int i = 0; i < arr.length(); i++) {
					JSONObject score = arr.getJSONObject(i); 
					String mods = Koohii.mods_str(score.getInt("enabled_mods"));
					Accuracy acc = new Accuracy(score.getInt("count300"), score.getInt("count100"), score.getInt("count50"), score.getInt("countmiss"));
					String info = "**`" + (mods.isEmpty() ? "No mods" : mods) + "` Score:\n"
							+ score.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + " " + (score.isNull("pp") ? "No " : score.getDouble("pp")) + "pp** | Score **" + score.getInt("score") + "**\n"
							+ df.format(acc.value() * 100) + "% " + (score.getInt("perfect") == 1 ? "FC " + score.getInt("maxcombo") + "x" : score.getInt("maxcombo") + "x") + " (" + acc.n300 + "/" + acc.n100 + "/" + acc.n50 + "/" + acc.nmisses + ")\n"
									+ "Played on " + score.getString("date") + (score.getInt("replay_available") == 1 ? " (Replay available)" : "");
					eb.appendDescription(info + "\n\n");
				}
				channel.sendMessage(eb.build()).queue();
				return new CommandResult(CommandResultType.SUCCESS);
			} catch (IOException e) {
				return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
			} catch (JSONException e) {
				e.printStackTrace();
				return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + uid + "` or the player has no scores on this map.");
			}
		}, "[username] [| index]", null, "Compare your score with the most recent map/score card in the channel!", Arrays.asList("  * Specify the index to skip to the nth recent score card in the channel."));
		
	}
	
	private static double weightForPP(double pp, String ibid, JSONArray userBest) {
		//String[] dataset = test.split(" ");
		//List<Double> datas = Arrays.asList(dataset).stream().map(s -> Double.parseDouble(s)).collect(Collectors.toList());
		
		double net = 0, change = -1;
		List<Double> pps = new ArrayList<>();
		for(int i = 0; i < userBest.length(); i++) {   //something like a treemap might work more elegantly, but i need to add pp to the list anyways so its fine i guess
		//for(int i = 0; i < datas.size(); i++) {
			JSONObject obj = userBest.getJSONObject(i);
			double v = obj.getDouble("pp");
			//double v = datas.get(i);
			//System.out.println("raw" + v);
			String bid = obj.getString("beatmap_id");
			if(bid.equals(ibid)) {
				if(v >= pp) {
					return -1;
				} else {
					change = v;
				}
			}
			if(v <= pp && !pps.contains(pp)) {
				pps.add(pp);
			}
			pps.add(v);
		}
		if(pps.indexOf(pp) == -1) {
			return 0;
		}
		//System.out.println(pps.indexOf(pp));
		if(change == -1) {
			double last = pps.size() > 100 ? pps.remove(100) * (Math.pow(0.95, 99)): 0;
			for(int i = pps.indexOf(pp); i < pps.size(); i++) {
				double c = pps.get(i);
				double w = c*(Math.pow(0.95, i));
				//System.out.println("pp" + w);
				if(i == pps.indexOf(pp)) {
					net += w;
				} else {
					net += c*(Math.pow(0.95, i - 1)) * (0.95 - 1);
				}
				//System.out.println("t1" + net);
			}
			//System.out.println("last" + last);
			net -= last;  //because the last one is completely not counted, not just shifted to a lower value
		} else {
			int old = pps.indexOf(change) - 1;
			pps.remove(change);
			for(int i = pps.indexOf(pp); i <= old; i++) {
				double c = pps.get(i);
				double w = c*(Math.pow(0.95, i));
				if(c == pp) {
					net += w - change*(Math.pow(0.95, old));	
				} else {
					net += w * (0.95 - 1);
				}
			}
		}	
		return net; 
	}
	
	private static JSONArray getUserBest(String id, int mode) {
		boolean noturl = false;
		try {
			new URL(id);
			id = id.substring(id.lastIndexOf("/") + 1);
		} catch (MalformedURLException e) {
			noturl = true;
		}
		JSONTokener result = null;
		InputStream stream = null;
		URL url = null;  
		try {
			String addparam = "";
			if (noturl) {
				addparam = "&type=string";
			}
			//System.out.println("https://osu.ppy.sh/api/get_user_best?k=" + osuAPI + "&m=" + mode + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam + "&limit=100");
			url = new URL(
					"https://osu.ppy.sh/api/get_user_best?k=" + osuAPI + "&m=" + mode + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam + "&limit=100");
			stream = url.openStream();
			if (stream.available() < 4) {
				throw new IllegalArgumentException("Unknown player `" + id + "`.");
			}
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		result = new JSONTokener(stream);
		return new JSONArray(result);
	}
	
	private static PPv2 constructPP(double aim_stars, double speed_stars, Map b, int mods) {
		PPv2Parameters p = new PPv2Parameters();
		p.aim_stars = aim_stars;
		p.speed_stars = speed_stars;
		p.beatmap = b;
		p.nobjects = b.objects.size();
		p.mods = mods;
		return new PPv2(p);
	}
	
	private static String getPlayer(User user) {  //say im getting from presence/name?
		try (Connection con = DiscordBot.db.getConnection()){
			ResultSet rs = con.createStatement().executeQuery("SELECT game FROM users WHERE id = " + user.getId() + ";");
			String player = rs.next() ? player = rs.getString(1).substring(0, rs.getString(1).indexOf("||")) : user.getName();
			rs.close();
			return player;  
		} catch (SQLException e) {
			return user.getName();
		}
	}
	
	private static int getMode(List<String> params) {
		int modeId = 0; String mode = "";
		params.replaceAll(String::toLowerCase);
		if(params.contains("-t")) {
			mode = "-t";
			modeId = 1;
		}
		if(params.contains("-c")) {
			mode = "-c";
			modeId = 2;
		}
		if(params.contains("-m")) {
			mode = "-m";
			modeId = 3;
		}
		params.removeAll(Collections.singleton(mode));  //pass by ref so it removes from the list 
		return modeId;
	}
	
	private static InputStream getMap(String origURL) throws IOException {
		String id = origURL.substring(origURL.lastIndexOf("/") + 1);
		if (origURL.contains("/beatmapsets/")) {
			if (!origURL.contains("#")) throw new IllegalArgumentException("Please specify a difficulty, instead of giving a set URL.");
		} else {
			if (origURL.contains("/s/")) throw new IllegalArgumentException("Please specify a difficulty, instead of giving a set URL.");
			id = id.split("&")[0];  //remove things like &m=0, which is no need
		}
		URLConnection mapURL;
		try {
			mapURL = new URL("https://osu.ppy.sh/osu/" + id).openConnection();
			mapURL.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0)");	
		} catch (IOException e) {
			//e.printStackTrace();
			throw new IllegalArgumentException("Invalid beatmap URL.");
		}
		try {
			return mapURL.getInputStream();
		} catch (IOException e) {
			if(e.getMessage().contains("503")) {   //most likely cloudflare, unless they change the request method which is very unlikely
				mapURL = new URL("https://bloodcat.com/osu/b/" + id).openConnection();
				mapURL.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0)");
				return mapURL.getInputStream();  //not catching this, if even this doesnt work just throw something went wrong.	
			} else {
				throw e;
			}
		}
	}
	
}
