package common.util;

import java.awt.Desktop;
import java.awt.GraphicsEnvironment;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;

import javax.imageio.ImageIO;
import javax.swing.JOptionPane;

import common.Version;
import common.collect.Lists;
import common.collect.Maps;
import common.log.Log;
import common.rng.Random;

public abstract class Util {
    private static final long START = getTime();
    private static final int MIN_JAVA_FEATURE = 21;
	public static final boolean WINDOWS = System.getProperty("os.name").toLowerCase(Locale.US).contains("win");
	public static final boolean DEVMODE = System.getProperty("runtime.devmode") != null;
	public static final int PROTOCOL = Version.MAJOR << 16 | Version.MINOR << 8 | Version.PATCH;
	public static final String VERSION = "v" + Version.MAJOR + "." + Version.MINOR + "." + Version.PATCH + Version.RELEASE;

	private static final String[] TIERS = new String[] {"II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"};

	private static boolean crashed;
    
	public static String strip(String str, int offset, int len, char newl, char tab, char unk) {
		StringBuilder sb = new StringBuilder();
		for(int pos = offset; pos < offset + len; pos++) {
			char c = str.charAt(pos);
			if(c == '\n') {
				if(newl == 0)
					return sb.toString();
				sb.append(newl);
			}
			else if((c == '\t') && tab != 0) {
				for(int z = 0; z < 4; z++)
					sb.append(tab);
			}
			else if(c == Log.CHR_UNK) {
				if(unk != 0)
					sb.append(unk);
			}
			else if(c >= Log.CHR_SPC && c <= 0xff) {
				sb.append(c);
			}
		}
		return sb.toString();
	}
	
	public static int compareLower(String str1, String str2) {
		if(str2.length() > str1.length())
			return 0;
		for(int z = 0; z < str2.length(); z++) {
			if(Character.toLowerCase(str1.charAt(z)) != Character.toLowerCase(str2.charAt(z)))
				return 0;
		}
		return str2.length() == str1.length() ? 0x7fffffff : str2.length();
	}
	
	public static Integer parseInt(String str, int base) {
		char c;
		boolean tbase = false;
		int digits = 0;
		long v = 0;
		long nbase = base != 0 ? (base < 0 ? -base : base) : 10;
		long nsign = 1;
		for(int pos = 0; pos < str.length(); pos++) {
			c = Character.toLowerCase(str.charAt(pos));
			if(pos == 0 && ((c == '+') || ((c == '-') && (base >= 0))))
				nsign = (c == '+') ? 1 : -1;
			else if(pos == 0 && c == '0') {
				tbase = true;
				digits++;
			}
			else if(tbase && pos == 1 && c == 'x') {
				nbase = 16;
				digits--;
			}
			else if(((nbase == 16) && (c >= 'a' && c <= 'f')) || (c >= '0' && c <= '9')) {
				v *= nbase;
				v += ((long)((c >= '0' && c <= '9') ? (c - '0') : (10 + (c - 'a'))));
				digits++;
			}
			else
				return null;
		}
		if(digits == 0)
			return null;
		v *= nsign;
		if((base < 0) ? (v < 0L || v > 0xffffffffL) : (v < -0x80000000L || v > 0x7fffffffL)) {
			return null;
		}
		return (int)((base < 0) ? (v & 0xffffffff) : (((v >> 32) & 0x80000000) | (v & 0x7fffffff)));
	}
	
	public static <T extends Enum> T parseEnum(Class<T> clazz, String str) {
		boolean name = Identifyable.class.isAssignableFrom(clazz);
		T[] values = clazz.getEnumConstants();
		Integer value;
		if((value = parseInt(str, 0)) != null && (value >= 0) && (value < values.length)) {
			return values[value];
		}
		int comp;
		int max = 0;
		T best = null;
		for(int z = 0; z < values.length; z++) {
			if((comp = compareLower(name ? ((Identifyable)values[z]).getName() : values[z].toString(), str)) > max) {
				max = comp;
				best = values[z];
			}
		}
		return best;
	}
	
	public static <T> T parseEnum(Class<T> base, String str, Class<? extends Enum> ... enums) {
		int comp;
		int max = 0;
		T best = null;
		for(Class<? extends Enum> clazz : enums) {
			if(!base.isAssignableFrom(clazz))
				throw new IllegalArgumentException("Klasse " + clazz.getSimpleName() + " ist nicht " + base.getSimpleName() + " untergeordnet");
			boolean name = Identifyable.class.isAssignableFrom(clazz);
			Enum<?>[] values = clazz.getEnumConstants();
			for(int z = 0; z < values.length; z++) {
				if((comp = compareLower(name ? ((Identifyable)values[z]).getName() : values[z].toString(), str)) > max) {
					max = comp;
					best = (T)values[z];
				}
			}
		}
		return best;
	}
	
	public static <T> int indexOf(T[] array, T elem) {
		for(int z = 0; z < array.length; z++) {
			if(array[z] == elem)
				return z;
		}
		return -1;
	}
	
	public static <T> int indexOfChecked(T[] array, T elem) {
		for(int z = 0; z < array.length; z++) {
			if(array[z] == elem)
				return z;
		}
		throw new IllegalArgumentException("Objekt ist nicht in Array");
	}
	
	public static Boolean parseBoolean(String str) {
		if("1".equals(str) || "true".equalsIgnoreCase(str) || "on".equalsIgnoreCase(str) || "yes".equalsIgnoreCase(str) || "y".equalsIgnoreCase(str))
			return true;
		else if("0".equals(str) || "false".equalsIgnoreCase(str) || "off".equalsIgnoreCase(str) || "no".equalsIgnoreCase(str) || "n".equalsIgnoreCase(str))
			return false;
		return null;
	}
	
	public static <T> String buildLines(String separator, Function<T, String> func, Iterable<T> elems) {
		StringBuilder sb = new StringBuilder();
		for(T elem : elems) {
			if(sb.length() > 0)
				sb.append(separator);
			sb.append(func.apply(elem));
		}
		return sb.toString();
	}

	public static <T> String buildLines(Function<T, String> func, Iterable<T> elems) {
		return buildLines("\n", func, elems);
	}
	
	public static <T> String buildLines(String separator, Iterable<T> elems) {
		return buildLines(separator, new Function<T, String>() {
			public String apply(T t) {
				return String.valueOf(t);
			}
		}, elems);
	}
	
	public static <T> String buildLines(Iterable<T> elems) {
		return buildLines("\n", elems);
	}
	
	public static <T> String buildLines(String separator, Function<T, String> func, T ... elems) {
		StringBuilder sb = new StringBuilder();
		for(T elem : elems) {
			if(sb.length() > 0)
				sb.append(separator);
			sb.append(func.apply(elem));
		}
		return sb.toString();
	}

	public static <T> String buildLines(Function<T, String> func, T ... elems) {
		return buildLines("\n", func, elems);
	}
	
	public static <T> String buildLines(String separator, T ... elems) {
		return buildLines(separator, new Function<T, String>() {
			public String apply(T t) {
				return String.valueOf(t);
			}
		}, elems);
	}
	
	public static <T> String buildLines(T ... elems) {
		return buildLines("\n", elems);
	}

	public static int mixColor(int c1, int c2) {
		return ((((c1 >> 24 & 255) + (c2 >> 24 & 255)) / 2) << 24) | ((((c1 >> 16 & 255) + (c2 >> 16 & 255)) / 2) << 16) | ((((c1 >> 8 & 255) + (c2 >> 8 & 255)) / 2) << 8) | 
				(((c1 & 255) + (c2 & 255)) / 2);
	}

	public static int mulColor(int color, float brightness) {
		int mul = ExtMath.clampi((int)(brightness * 255.0f), 0, 255);
		return (color & 0xff000000) | ((((color >> 16 & 255) * mul) / 255) << 16) | ((((color >> 8 & 255) * mul) / 255) << 8) | 
				(((color & 255) * mul) / 255);
	}
	
	public static void checkPlatform() {
		int feature;
		try {
			feature = Runtime.version().feature();
		}
		catch(Throwable t) {
			feature = 0;
		}
		String info = null;
		String msg = null;
		if(System.getProperty("os.name").startsWith("Mac")) {
			info = "Inkompatibles Betriebssystem";
			msg = "Linux, *BSD oder Windows ist erforderlich, um dieses Programm\n" + 
					"auszuführen. Alle Versionen von Mac OS (X) sind nicht kompatibel.";
		}
		else if(feature < MIN_JAVA_FEATURE) {
			info = "Inkompatible Java-Version";
			msg = "Java " + MIN_JAVA_FEATURE + " oder höher ist erforderlich, um dieses Programm auszuführen.";
		}
		if(info != null) {
			System.err.println("#################################################################");
			System.err.println("*** " + info + " ***");
			System.err.println(msg);
			System.err.println("#################################################################");
			if(!GraphicsEnvironment.isHeadless())
				JOptionPane.showMessageDialog(null, msg, info, JOptionPane.ERROR_MESSAGE);
			System.exit(1);
		}
	}
	
	public static void setupHandlers() {
		Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
			public void uncaughtException(Thread thread, Throwable e) {
				System.err.println("Fehler in Thread '" + thread.getName() + "'");
				e.printStackTrace(System.err);
				if(crashed)
					System.exit(1);
				if(e instanceof OutOfMemoryError) {
					System.gc();
					System.gc();
				}
				if(!thread.getName().startsWith("Thread-") || e instanceof OutOfMemoryError) {
					System.err.println("Beende!");
					crashed = true;
					if(!DEVMODE) {
						PrintStream ps = null;
						File report = null;
						try {
							Date date = new Date();
							File file = new File("crash-" + new SimpleDateFormat("dd.MM.yyyy_HH.mm.ss").format(date) + ".txt");
							FileOutputStream out = new FileOutputStream(file);
							ps = new PrintStream(out);
							ThreadMXBean bean = ManagementFactory.getThreadMXBean();
							ThreadInfo[] info = bean.dumpAllThreads(true, true);
							StringBuilder sb = new StringBuilder();
							Error error = new Error();
							for(ThreadInfo threadinfo : info) {
								if(threadinfo.getThreadId() == thread.threadId())
									error.setStackTrace(threadinfo.getStackTrace());
								sb.append(threadinfo);
							}
							ps.println("************************************************ " + new SimpleDateFormat("dd.MM.yyyy HH:mm:ss").format(date) + " ************************************************");
							ps.println();
							ps.println("\"Wie haste das denn nu wieder geschafft, Bursche?\"");
							ps.println("Unerwarteter Fehler in Thread '" + thread.getName() + "':");
							e.printStackTrace(ps);
							ps.println();
							ps.println("---------------------------------------------- Thread-Dump (" + info.length + " Threads) ---------------------------------------------");
							ps.println();
							ps.print(sb.toString());
							ps.println("*********************************************************************************************************************");
							report = file;
						}
						catch(Throwable t) {
							System.err.println("Konnte Absturzbericht nicht speichern:");
							t.printStackTrace(System.err);
						}
						finally {
							if(ps != null)
								ps.close();
						}
						if(report != null) {
							System.err.println("Absturzbericht gespeichert unter " + report.getPath());
							try {
								Desktop.getDesktop().browse(report.toURI());
							}
							catch(Throwable e1) {
								System.err.println("Konnte " + report + " nicht öffnen: " + e1);
							}
						}
					}
					System.exit(1);
				}
			}
		});
		ImageIO.setUseCache(false);
		Thread timer = new Thread("Timer Hack Thread") {
			public void run() {
				while(true) {
					try {
						Thread.sleep(2147483647L);
					}
					catch(InterruptedException e) {
						;
					}
				}
			}
		};
		timer.setDaemon(true);
		timer.start();
		System.setProperty("java.net.preferIPv4Stack", "true");
	}
	
	public static void addShutdownHook(final Runnable hook) {
		Runtime.getRuntime().addShutdownHook(new Thread("Game Shutdown Thread") {
			public void run() {
				if(!crashed) {
					try {
						hook.run();
					}
					catch(Throwable e) {
						e.printStackTrace();
					}
				}
			}
		});
	}

	// plr_play("/home/sen/Musik/midi50k/Video_Games/ff/ff2cecil.mid", 0);
	public static void meltdown() {
		Random rand = new Random();
		Log.SYSTEM.error("CORE_MELTDOWN: Nuclear processor core meltdown imminent\n\n" + 
			"  ************************ CAUTION ************************\n" + 
			"       KCTL: Processor core #%02d has reached a critical\n" + 
			"         temperature, system explosion is imminent!   \n" + 
			"           According to the general core density      \n" + 
			"           calculation routine defined by the SKC     \n" + 
			"          (Hard) Warfare Testing Facility (SKC-WTF)   \n" + 
			"         your processor will cause a detonation with  \n" + 
			"            a radius of (roughly) %d.%d kilometers.     \n" + 
			"          In addition, it will release appoximately   \n" + 
			"           %d megajoules of ionizing radiation.    \n" + 
			"         You have an estimate time of %d minutes and   \n" + 
			"        %d seconds left to clear the detonation area. \n" + 
			"  ************************ CAUTION ************************\n"
		, rand.range(1, 64), rand.range(1, 9), rand.range(0, 9), rand.range(10000, 39999), rand.range(3, 9), rand.range(2, 59));
	}
	
	public static Pair<String, String> getKeyValue(String text, char separator) {
		int index = text.indexOf(separator);
		if(index == -1)
			return new Pair<String, String>(text, null);
		return new Pair<String, String>(text.substring(0, index), text.substring(index + 1));
	}
	
	public static Pair<String, String> getKeyValue(String text) {
		return getKeyValue(text, ' ');
	}
	
	private static Object parseObject(String arg) {
		try {
			return Float.parseFloat(arg);
		}
		catch(NumberFormatException e) {
		}
		try {
			return Integer.parseInt(arg);
		}
		catch(NumberFormatException e) {
		}
		return arg;
	}
	
	public static Map<String, Object> parseArgsSimple(String[] args) {
		Map<String, Object> map = Maps.newHashMap();
		List<Object> list = Lists.newArrayList();
		String option = null;
		boolean parse = true;
		for(int z = 0; z < args.length; z++) {
			String arg = args[z];
			if(arg.startsWith("--")) {
				if(arg.length() == 2) {
					parse = false;
					continue;
				}
				if(option != null)
					map.put(option, null);
				option = arg.substring(2);
			}
			else if(arg.startsWith("-") && arg.length() == 2) {
				if(option != null)
					map.put(option, null);
				option = arg.substring(1);
			}
			else if(option != null) {
				map.put(option, parseObject(arg));
				option = null;
			}
			else {
				list.add(parseObject(arg));
			}
		}
		if(option != null)
			map.put(option, null);
		map.put("__pos", list);
		return map;
	}

	public static long getTime() {
		return System.nanoTime() / 1000L; // glfwGetTimerValue() / (glfwGetTimerFrequency() / 1000000L);
	}
	
	public static long rtime() {
		return Util.getTime() - START;
	}
	
	public static double ftime() {
		return ((double)rtime()) / 1000000.0;
	}

	public static String getRegionFolder(int x, int z) {
		return String.format("%c%03X%c%03X", x < 0 ? 'n' : 'p', ((x < 0) ? -x : x) >> 9, z < 0 ? 'n' : 'p', ((z < 0) ? -z : z) >> 9);
	}

	public static String getRegionName(int x, int z) {
		return String.format("r.%c%X%c%X.rgn", x < 0 ? 'n' : 'p', ((x < 0) ? -x : x) >> 3, z < 0 ? 'n' : 'p', ((z < 0) ? -z : z) >> 3);
	}
	
	public static String repeatString(Object obj, int count) {
		StringBuilder sb = new StringBuilder();
		for(int z = 0; z < count; z++) {
			sb.append(obj);
		}
		return sb.toString();
	}

	public static String getHexString(byte[] bytes) {
		StringBuilder sb = new StringBuilder();
		for(int z = 0; z < bytes.length; z++) {
			sb.append(String.format("%02x", bytes[z]));
		}
		return sb.toString();
	}

	public static byte[] fromHexString(String str) {
		if((str.length() & 1) == 1)
			str = "0" + str;
		byte[] bytes = new byte[str.length() / 2];
		try {
			for(int z = 0; z < bytes.length; z++) {
				bytes[z] = (byte)Integer.parseUnsignedInt(str.substring(z * 2, (z + 1) * 2), 16);
			}
		}
		catch(NumberFormatException e) {
			return null;
		}
		return bytes;
	}
	
	public static String breakString(String str, int width) {
		StringBuilder sb = new StringBuilder();
		for(int z = 0; z < str.length() / width; z++) {
			sb.append(str.substring(z * width, (z + 1) * width)).append('\n');
		}
		return sb.append(str.substring(str.length() - (str.length() % width), str.length())).toString();
	}
	
	public static String sanitizeCommonName(String str) {
		str = str.trim();
		StringBuilder sb = new StringBuilder();
		boolean hyphen = true;
		for(int z = 0; z < str.length(); z++) {
			char ch = Character.toLowerCase(str.charAt(z));
			if((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) {
				sb.append(ch);
				hyphen = false;
			}
			else if(ch == '-' || ch == ' ') {
				if(!hyphen)
					sb.append('-');
				hyphen = true;
			}
		}
		return sb.length() > 0 && sb.charAt(sb.length() - 1) == '-' ? sb.substring(0, sb.length() - 1) : sb.toString();
	}
	
	public static boolean isValidCommonName(String str) {
		boolean hyphen = true;
		for(int z = 0; z < str.length(); z++) {
			char ch = Character.toLowerCase(str.charAt(z));
			if((ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') && ch != '-')
				return false;
			if(ch == '-' && hyphen)
				return false;
			hyphen = ch == '-';
		}
		return !hyphen;
	}

	public static void throwUnchecked(Throwable t) {
	    throw t instanceof RuntimeException ? (RuntimeException)t : new RuntimeException(t);
	}
	
	public static String getTierSuffix(int tier) {
		return tier <= 0 ? "" : " " + (tier > TIERS.length ? (tier + 1) : TIERS[tier - 1]);
	}
}
