package shadersmod.installer;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Scanner;

import javax.swing.JOptionPane;

import shadersmod.common.SMCVersion;
import shadersmod.installer.Json.JsArray;
import shadersmod.installer.Json.JsObject;
import shadersmod.installer.Json.JsString;
import shadersmod.installer.Json.JsValue;

public class Installer {

	public static void main(String[] args) {
		Installer inst = new Installer();
		inst.run();
	}

	MessageDigest mdSha;
	StringBuilder logMessage = new StringBuilder();
	
	static final String mcVer  = SMCVersion.mcVersion;
	static final String modVer = SMCVersion.versionString;
//	static final String ofVer  = "HD_U_D1";
	static final String friendlyModName = "ShadersMod";
	static final String libraryModDir  = "shadersmodcore";
	static final String libraryModName = "ShadersModCore";
	static final String libraryModVer  = modVer+"mc"+mcVer;
	static final String libraryModDesc = libraryModDir+":"+libraryModName+":"+libraryModVer;
	static final String versionIn       = mcVer;
	static final String versionMod      = mcVer+"-ShadersMod"+modVer;
	static final String profileMod      = mcVer+"-ShadersMod";
	static final String modReleaseTime = "2017-06-17T18:00:00+07:00";
//	static final String versionOFIn     = mcVer+"-OptiFine_"+ofVer;
//	static final String versionOFMod    = mcVer+"-OptiFine_"+ofVer+"-ShadersMod"+modVer;
//	static final String profileOFMod    = mcVer+"-OptiFine-ShadersMod";

//	static boolean enabledOF = true;

	public void run() {
		try {
			mdSha = MessageDigest.getInstance("SHA");
		} catch (NoSuchAlgorithmException e) {
			System.err.println("No SHA algorithm");
			mdSha = null;
		}
		File mcDir = getMcDir();
		
		String[] vers = new File(mcDir,"versions").list(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String name) {
				return (name.equals(mcVer) 
//						|| name.startsWith(mcVer+"-OptiFine_HD_U_")
						    && (name.indexOf("ShadersMod")==-1))
						&& (new File(dir,name).isDirectory());
			}});

		String message = String.format("Minecraft directory %s\n\nSelect base version.", mcDir.getAbsolutePath());
		String title = friendlyModName+" "+modVer+" Installer for Minecraft "+mcVer;
		int option = JOptionPane.showConfirmDialog(null, "Make sure you have closed Minecraft and Minecraft Launcher", title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
		if (option == JOptionPane.OK_OPTION) {
			Object baseVer = JOptionPane.showInputDialog(null, message, title, JOptionPane.PLAIN_MESSAGE, null, vers, mcVer);
			if (baseVer instanceof String) {
					startInstall(mcDir, (String)baseVer);
			}
		}
		//int option = JOptionPane.showConfirmDialog(null, message, , JOptionPane.YES_NO_OPTION, JOptionPane.PLAIN_MESSAGE);
		//if (option == JOptionPane.YES_OPTION) {
		//	startInstall(mcDir);
		//}
	}
	
	public File getMcDir() {
		String osName = System.getProperty("os.name").toLowerCase();
		String userHome = System.getProperty("user.home",".");
		if (osName.contains("win")) {
			String baseDir = System.getenv("APPDATA");
			if (baseDir == null) {
				baseDir = userHome;
			}
			return new File(baseDir, ".minecraft/");
		} else if (osName.contains("mac")) {
			return new File(userHome, "Library/Application Support/minecraft");
		} else if (osName.contains("linux") || osName.contains("unix")) {
			return new File(userHome, ".minecraft/");
		} else {
			return new File(userHome, "minecraft/");
		}
	}
	
	public void startInstall(File mcDir, String baseVer) {
		boolean result = false;
		if (installLibraries(mcDir)) {
			String versionMod = baseVer+"-ShadersMod"+modVer;
			if (installVersion(mcDir, baseVer, versionMod)) {
				if (installProfile(mcDir, profileMod, versionMod)) {
					result = true;
				}
			}
		}
		logMessage.append(result? "complete": "fail");
		JOptionPane.showMessageDialog(null, logMessage, "Result", JOptionPane.PLAIN_MESSAGE);
	}

	public void startInstall(File mcDir) {
		boolean result = false;
		if (installLibraries(mcDir)) {
			if (installVersion(mcDir, versionIn, versionMod)) {
				if (installProfile(mcDir, profileMod, versionMod)) {
					result = true;
				}
			}
//			if (enabledOF) {
//				if (installVersion(mcDir, versionOFIn, versionOFMod)) {
//					if (installProfile(mcDir, profileOFMod, versionOFMod)) {
//						result = true;
//					}
//				}
//			}
		}
		logMessage.append(result? "complete": "fail");
		JOptionPane.showMessageDialog(null, logMessage, "Result", JOptionPane.PLAIN_MESSAGE);
	}
	
	void close(Closeable c) {
		if (c != null) {
			try {
				c.close();
			} catch(IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	int readLoop(InputStream inp, byte[] buffer, int size) throws IOException {
		int pos = 0;
		while (pos < size) {
			int n = inp.read(buffer, pos, size-pos);
			if (n<=0) break;
			pos += n;
		}
		return pos;
	}
	
	byte[] readToByteArray(InputStream inp) throws IOException {
		final int bufSize = 1048576;
		ArrayList<byte[]> buffers = new ArrayList<byte[]>();
		int bufIndex = 0;
		int pos = 0;
		int nread = 0;
		do {
			byte[] buf = new byte[bufSize];
			buffers.add(buf);
			pos = 0;
			do {
				nread = 0;
				nread = inp.read(buf,pos,bufSize-pos);
				if (nread <= 0)
					break;
				pos += nread;
			} while (pos < bufSize);
			if (nread <= 0)
				break;
			++bufIndex;
		} while (nread > 0);
		byte[] abyte = new byte[bufIndex*bufSize+pos];
		int i;
		for (i=0; i<bufIndex; ++i)
			System.arraycopy(buffers.get(i), 0, abyte, i*bufSize, bufSize);
		System.arraycopy(buffers.get(i), 0, abyte, i*bufSize, pos);
		return abyte;
	}
	byte[] getResourceByteArray(String name)
	{
		return readByteArray(getClass().getResourceAsStream(name));
	}
	byte[] readByteArray(InputStream inp) {
		byte[] abyte = null;
		if (inp!=null) {
			final int bufSize = 1048576;
			ArrayList<byte[]> buffers = new ArrayList<byte[]>();
			int bufIndex = 0;
			int pos = 0;
			int nread = 0;
			try {
				do {
					bufIndex = buffers.size();
					byte[] buf = new byte[bufSize];
					buffers.add(buf);
					pos = 0;
					do {
						nread = 0;
						nread = inp.read(buf,pos,bufSize-pos);
						if (nread <= 0)
							break;
						pos += nread;
					} while (pos < bufSize);
				} while (nread > 0);
			} catch (IOException e) {
				e.printStackTrace();
			}
			close(inp);
			abyte = new byte[bufIndex*bufSize+pos];
			int i;
			for (i=0; i<bufIndex; ++i)
				System.arraycopy(buffers.get(i), 0, abyte, i*bufSize, bufSize);
			System.arraycopy(buffers.get(i), 0, abyte, i*bufSize, pos);		
		}
		return abyte;
	}
	String getResourceString(String resName)
	{
		InputStream in = getClass().getResourceAsStream(resName);
		BufferedReader reader = new BufferedReader(new InputStreamReader(in));
		String line = "";
		try {
			line = reader.readLine();
			reader.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return line;
	}

	public boolean writeFileWithSha(File dst, byte[] abyte)
	{
		boolean result = false;
		OutputStream out = null;
		Writer wr = null;
		try {
			out = new FileOutputStream(dst);
			out.write(abyte);
			out.close(); out = null;
			result = true;
			wr = new FileWriter(dst.getPath()+".sha");
			wr.write(toShaString(abyte));
			wr.close(); wr = null;
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		close(out); close(wr);
		return result;
	}

	public boolean writeFile(File dst, byte[] abyte)
	{
		boolean result = false;
		OutputStream out = null;
		try {
			out = new FileOutputStream(dst);
			out.write(abyte);
			out.close(); out = null;
			result = true;
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		close(out);
		return result;
	}
	
	public void copyFile(File src, File dst)
	{
		InputStream  inp = null;
		OutputStream out = null;
		byte[] abyte = null;
		try {
			int size = (int)src.length();
			abyte = new byte[size];
			inp = new FileInputStream(src);
			readLoop(inp, abyte, size);
			inp.close(); inp = null;
			
			out = new FileOutputStream(dst);
			out.write(abyte);
			out.close(); out = null;
			
			dst.setLastModified(src.lastModified());
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		close(inp); close(out);
	}
	
	public boolean copyFileWithSha(File src, File dst)
	{
		boolean result = false;
		InputStream  inp = null;
		OutputStream out = null;
		Writer wr = null;
		byte[] abyte = null;
		try {
			int size = (int)src.length();
			abyte = new byte[size];
			inp = new FileInputStream(src);
			readLoop(inp, abyte, size);
			inp.close(); inp = null;
			out = new FileOutputStream(dst);
			out.write(abyte);
			out.close(); out = null;
			dst.setLastModified(src.lastModified());
			result = true;
			wr = new FileWriter(dst.getPath()+".sha");
			wr.write(toShaString(abyte));
			wr.close(); wr = null;
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		close(inp); close(out); close(wr);
		return result;
	}
	
	public String toShaString(byte[] abyte)
	{
		mdSha.reset();
		byte[] sha = mdSha.digest(abyte);
		StringBuilder sb = new StringBuilder();
		for (byte b : sha)
			sb.append(String.format("%02x",b));
		return sb.toString();
	}
	private InputStream getJarAsStream()
	{
		URL classUrl = this.getClass().getClassLoader().getResource(this.getClass().getName().replace('.','/') + ".class");
		//logMessage.append(this.getClass().getName());
		//logMessage.append(classUrl!=null);
		if (classUrl != null) {
			String classUrlStr = classUrl.toString();
			//logMessage.append(classUrlStr);
			int endIndex = classUrlStr.indexOf("!/");
			if (endIndex != -1) {
				if (classUrlStr.startsWith("jar:")) {
					try {
						URL jarUrl = new URL(classUrlStr.substring(4, endIndex));
						//logMessage.append(jarUrl.toString()).append("\n");
						InputStream inputStream = jarUrl.openStream();
						//logMessage.append((inputStream!=null));
						return inputStream;
					} catch (Exception e) {
					}
				}
			}
		}
		return null;
	}
	public boolean installSelfLibrary(File mcDir, String subDir, String name, String version)
	{
		boolean result = false;
		File outDir = new File(mcDir,"libraries/"+subDir+"/"+name+"/"+version);
		File outJar = new File(outDir,name+"-"+version+".jar");
		//File outSha = new File(outDir,name+"-"+version+".jar.sha");
		outDir.mkdirs();
		byte[] abyte = null;
		InputStream inputStream = getJarAsStream();
		if (inputStream == null)
			inputStream = getClass().getResourceAsStream("/"+name+"-"+version+".jar");
		if (inputStream != null);
			abyte = readByteArray(inputStream);
		if (abyte!=null) {
			if (writeFileWithSha(outJar, abyte)) {
				result = true;
			}
		}
		return result;
	}
	public boolean installLibrary(File mcDir, String subDir, String name, String version)
	{
		boolean result = false;
		File outDir = new File(mcDir,"libraries/"+subDir+"/"+name+"/"+version);
		File outJar = new File(outDir,name+"-"+version+".jar");
		//File outSha = new File(outDir,name+"-"+version+".jar.sha");
		outDir.mkdirs();
		byte[] abyte = readByteArray(getClass().getResourceAsStream("/"+name+"-"+version+".jar"));
		if (abyte!=null) {
			if (writeFileWithSha(outJar, abyte)) {
				result = true;
			}
		}
		return result;
	}
	public boolean installLibraries(File mcDir)
	{
		if (installSelfLibrary(mcDir, libraryModDir,libraryModName,libraryModVer)) {
			return true;
			//showTextResource("/License-ASM.txt","License-ASM");
			//if (installLibrary(mcDir, "org/ow2/asm","asm-all","4.1")) {
			//	return true;
			//}
		}
		return false;
	}
	public void showTextResource(String resName, String title)
	{
		InputStream in = getClass().getResourceAsStream(resName);;
		BufferedReader reader = new BufferedReader(new InputStreamReader(in));
		StringBuilder text = new StringBuilder(); 
		String line;
		try {
			while ((line = reader.readLine()) != null) {
				text.append(line).append("\n");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		try {
			reader.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		JOptionPane.showMessageDialog(null, text, title, JOptionPane.PLAIN_MESSAGE);
	}
//	public boolean writeVersionJson1(File inpJson, File outJson)
//	{
//		boolean result = false;
//		BufferedReader inr = null;
//		try {
//			inr = new BufferedReader(new FileReader(inpJson));
//			ByteArrayOutputStream  baos = new ByteArrayOutputStream(); 
//			PrintStream ps = new PrintStream(baos);
//			String line = null;
//			while ((line = inr.readLine())!= null) {
//				if (line.startsWith("  \"minecraftArguments\"")) {
//					int i = line.lastIndexOf('"');
//					line = line.substring(0, i) + " --tweakClass shadersmodcore.loading.SMCTweaker" + line.substring(i);
//					ps.print(line); ps.print('\n');
//				} else if (line.equals("  \"libraries\": [")) {
//					ps.print(line); ps.print('\n');
//					ps.print(
//							"    {\n"+
//							"      \"name\": \"shadersmodcore:ShadersModCore:2.3.14mc1.7.5\"\n"+
//							"    },\n"+
//							"    {\n"+
//							"      \"name\": \"net.minecraft:launchwrapper:1.9\"\n"+
//							"    },\n"+
//							"    {\n"+
//							"      \"name\": \"org.ow2.asm:asm-all:4.1\"\n"+
//							"    },\n"								
//							);
//				} else {
//					ps.print(line); ps.print('\n');
//				}
//			}
//			inr.close(); inr = null;
//			ps.close(); ps = null;
//			byte[] abyte = baos.toByteArray(); 
//			if (writeFileWithSha(outJson, abyte));
//				result = true;
//		} catch (FileNotFoundException e) {
//			e.printStackTrace();
//		} catch (IOException e) {
//			e.printStackTrace();
//		}
//		close(inr);
//		return result;
//	}
	JsObject libraryAsJso(String library) {
		JsObject jso = new JsObject();
		jso.put("name",new JsString(library));
		return jso;
	}
	public boolean writeVersionJson(File inpJson, File outJson, String outVersion)
	{
		boolean result = false;
		BufferedReader inr = null;
		try {
			inr = new BufferedReader(new FileReader(inpJson));
			JsonParser jp = new JsonParser(inr);
			Json.JsObject jso = jp.getObject();
			inr.close(); inr = null;
			// edit
			jso.put("id",new JsString(outVersion));
			jso.put("mainClass",new JsString("net.minecraft.launchwrapper.Launch"));
			JsString jssArg = (JsString)jso.get("minecraftArguments");
			jssArg.str = jssArg.str + " --tweakClass shadersmod.launch.SMCTweaker";
			JsArray jsaLibs = (JsArray)jso.get("libraries");;
			ArrayList<JsValue> ao1 = jsaLibs.elements;
			ArrayList<JsValue> ao2 = new ArrayList<JsValue>();  
			jsaLibs.elements = ao2;
			ao2.add(libraryAsJso(libraryModDesc));
			ao2.add(libraryAsJso("net.minecraft:launchwrapper:1.12"));
			ao2.add(libraryAsJso("org.ow2.asm:asm-all:5.2"));
			int i, max=ao1.size();
			for (i=0; i<max; ++i) {
				JsObject jsoLib = (JsObject) ao1.get(i);
				JsString jssLibName = (JsString) jsoLib.get("name");
				String libName = jssLibName.str;
				if (libName==null) {
				} else if (libName.startsWith("shadersmodcore:ShadersModCore:")) {
				} else if (libName.startsWith("net.minecraft:launchwrapper:")) {
				} else if (libName.startsWith("org.ow2.asm:asm-all:")) {
				} else {
					ao2.add(jsoLib);
				}
			}
			// print
			ByteArrayOutputStream  baos = new ByteArrayOutputStream(); 
			PrintWriter pw = new PrintWriter(baos);
			jso.print(pw,0);
			pw.close(); pw = null;
			byte[] abyte = baos.toByteArray(); 
			if (writeFileWithSha(outJson, abyte));
				result = true;
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		close(inr);
		return result;
	}
	public boolean createVersionJson(File inpJson, File outJson, String outVersion)
	{
		boolean result = false;
		BufferedReader inr = null;
		try {
			inr = new BufferedReader(new FileReader(inpJson));
			JsonParser jp = new JsonParser(inr);
			Json.JsObject jsi = jp.getObject();
			Json.JsObject jso = new Json.JsObject();
			inr.close(); inr = null;
			// edit
			jso.put("id", new JsString(outVersion));
			String time = java.time.ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
			jso.put("time", new JsString(time));
			jso.put("type", new JsString("release"));
			jso.put("releaseTime", new JsString(getResourceString("/shadersmod/installer/releaseTime")));
			jso.put("inheritsFrom", new JsString(mcVer));
			jso.put("mainClass", new JsString("net.minecraft.launchwrapper.Launch"));
			jso.put("minecraftArguments", new JsString("--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}"));
			JsString jssArg = (JsString)jso.get("minecraftArguments");
			jssArg.str = jssArg.str + " --tweakClass shadersmod.launch.SMCTweaker";
			jso.put("libraries", new JsArray());
			ArrayList<JsValue> ao = ((JsArray)jso.get("libraries")).elements;
			ao.add(libraryAsJso(libraryModDesc));
			ao.add(libraryAsJso("net.minecraft:launchwrapper:1.12"));
			ao.add(libraryAsJso("org.ow2.asm:asm-all:5.2"));
			// print
			ByteArrayOutputStream  baos = new ByteArrayOutputStream(); 
			PrintWriter pw = new PrintWriter(baos);
			jso.print(pw,0);
			pw.close(); pw = null;
			byte[] abyte = baos.toByteArray(); 
			if (writeFileWithSha(outJson, abyte));
				result = true;
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		close(inr);
		return result;
	}
	
	public boolean installVersion(File mcDir, String inpVersion, String outVersion)
	{
		File inpDir  = new File(mcDir,"versions/"+inpVersion+"/");
		File inpJar  = new File(inpDir,inpVersion+".jar" );
		File inpJson = new File(inpDir,inpVersion+".json");
		File outDir  = new File(mcDir,"versions/"+outVersion+"/");
		File outJar  = new File(outDir,outVersion+".jar" );
		File outJson = new File(outDir,outVersion+".json");
		if (inpJar.exists() && inpJson.exists()) {
			outDir.mkdirs();
			if (copyFileWithSha(inpJar, outJar)) {
				if (createVersionJson(inpJson, outJson, outVersion)) {
					String str = String.format("Version %s => %s\n", inpVersion, outVersion);
					logMessage.append(str);
					return true;
				}
			}
			return false;
		} else {
			String str = String.format("Cannot find version %s .. skip\n", inpVersion);
			logMessage.append(str);
			return false;
		}
	}
	
//	public boolean testVersion(File mcDir, String inpVersion, String outVersion)
//	{
//		File inpDir  = new File(mcDir,"versions/"+inpVersion+"/");
//		File inpJar  = new File(inpDir,inpVersion+".jar" );
//		File inpJson = new File(inpDir,inpVersion+".json");
//		File outDir  = new File(mcDir,"versions/"+outVersion+"/");
//		File outJar  = new File(outDir,outVersion+".jar" );
//		File outJson = new File(outDir,outVersion+".json");
//		if (inpJar.exists() && inpJson.exists()) {
//			BufferedReader rd = null;
//			try {
//				rd = new BufferedReader(new FileReader(inpJson));
//				JsonParser jp = new JsonParser(rd);
//				Json.JsObject jso = jp.getObject();
//				Writer wr = new PrintWriter(System.out);
//				jso.print(wr, 0);
//				wr.close();
//			} catch (FileNotFoundException e) {
//				e.printStackTrace();
//			} catch (IOException e) {
//				e.printStackTrace();
//			}
//			close(rd);
//			return false;
//		} else {
//			String message = String.format("Cannot find version %s\nPlease run Minecraft %s before this installer.", inpVersion, inpVersion);
//			JOptionPane.showMessageDialog(null, message, "Result", JOptionPane.PLAIN_MESSAGE);
//			return false;
//		}
//	}
	public boolean installProfile(File mcDir, String profile, String version)
	{
		boolean result = false;
		File inpJson = new File(mcDir,"launcher_profiles.json");
		File outJson = inpJson;
		if (inpJson.exists()) {
			BufferedReader inr = null;
			try {
				inr = new BufferedReader(new FileReader(inpJson));
				JsonParser jp = new JsonParser(inr);
				JsObject jsoRoot = jp.getObject();
				inr.close(); inr = null;
				// edit
				JsObject jsoProfiles = (JsObject)jsoRoot.get("profiles");
				JsObject jsoModProfile = (JsObject)jsoProfiles.get(profile);
				if (jsoModProfile == null) {
					jsoModProfile = new JsObject();
					jsoModProfile.put("name",new JsString(profile));
					jsoModProfile.put("lastVersionId",new JsString(version));
					jsoModProfile.put("useHopperCrashService",new Json.JsBoolean("false"));
					jsoModProfile.put("launcherVisibilityOnGameClose",new JsString("keep the launcher open"));
					jsoProfiles.put(profile, jsoModProfile);
				} else {
					jsoModProfile.put("name",new JsString(profile));
					jsoModProfile.put("lastVersionId",new JsString(version));
				}
				jsoRoot.put("selectedProfile", new JsString(profile));
				// print
				ByteArrayOutputStream  baos = new ByteArrayOutputStream(); 
				PrintWriter pw = new PrintWriter(baos);
				jsoRoot.print(pw,0);
				pw.close(); pw = null;
				byte[] abyte = baos.toByteArray(); 
				if (writeFile(outJson, abyte));
				{
					result = true;
					logMessage.append(String.format("Add profile %s for version %s\n", profile, version));
				}
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
			close(inr);
		}
		return result;
	}

}
