package net.oni2.aeinstaller.backend.oni.management;

import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Vector;
import java.util.regex.Pattern;

import net.oni2.SettingsManager;
import net.oni2.aeinstaller.AEInstaller2;
import net.oni2.aeinstaller.backend.CaseInsensitiveFile;
import net.oni2.aeinstaller.backend.Paths;
import net.oni2.aeinstaller.backend.RuntimeOptions;
import net.oni2.aeinstaller.backend.oni.OniSplit;
import net.oni2.aeinstaller.backend.oni.PersistDat;
import net.oni2.aeinstaller.backend.oni.XMLTools;
import net.oni2.aeinstaller.backend.oni.management.tools.ToolFileIterator;
import net.oni2.aeinstaller.backend.oni.management.tools.ToolFileIteratorEntry;
import net.oni2.aeinstaller.backend.oni.management.tools.ToolInstallationList;
import net.oni2.aeinstaller.backend.packages.EBSLInstallType;
import net.oni2.aeinstaller.backend.packages.Package;
import net.oni2.aeinstaller.backend.packages.PackageManager;
import net.oni2.platformtools.PlatformInformation;
import net.oni2.platformtools.PlatformInformation.Platform;
import net.oni2.platformtools.applicationinvoker.ApplicationInvocationResult;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.javabuilders.swing.SwingJavaBuilder;

import com.paour.NaturalOrderComparator;

/**
 * @author Christian Illy
 */
public class Installer {
	private static FileFilter dirFileFilter = new FileFilter() {
		@Override
		public boolean accept(File pathname) {
			return pathname.isDirectory();
		}
	};

	/**
	 * Verify that the Edition is within a subfolder to vanilla Oni
	 * (..../Oni/Edition/AEInstaller)
	 * 
	 * @return true if GDF can be found in the parent's parent-path
	 */
	public static boolean verifyRunningDirectory() {
		return Paths.getVanillaGDF().exists()
				&& Paths.getVanillaGDF().isDirectory();
	}

	/**
	 * @return Is Edition Core initialized
	 */
	public static boolean isEditionInitialized() {
		return Paths.getVanillaOnisPath().exists();
	}

	/**
	 * Install the given set of mods
	 * 
	 * @param mods
	 *            Mods to install
	 * @param listener
	 *            Listener for install progress updates
	 */
	public static void install(TreeSet<Package> mods,
			InstallProgressListener listener) {
		File logFile = new File(Paths.getInstallerPath(), "Installation.log");
		Logger log = null;
		try {
			log = new Logger(logFile);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		Date start = new Date();
		log.println("Installation of mods started at " + sdf.format(start));

		log.println();
		log.println("AEI2 version: "
				+ SwingJavaBuilder.getConfig().getResource("appversion"));

		ToolInstallationList til = ToolInstallationList.getInstance();
		log.println("Installed tools:");
		for (Package t : PackageManager.getInstance().getInstalledTools()) {
			log.println(String.format(" - %s (%s)", t.getName(), t.getVersion())
					+ (til.isModified(t.getPackageNumber()) ? " (! LOCALLY MODIFIED !)"
							: ""));
		}
		log.println("Installing mods:");
		for (Package m : mods) {
			log.println(String.format(" - %s (%s)", m.getName(), m.getVersion()));
		}
		log.println();

		HashSet<String> levelsAffectedBefore = null;
		if (ModInstallationList.getInstance().isLoadedFromFile()) {
			levelsAffectedBefore = ModInstallationList.getInstance()
					.getAffectedLevels();
		}
		HashSet<String> levelsAffectedNow = new HashSet<String>();

		File IGMD = new File(Paths.getEditionGDF(), "IGMD");
		if (IGMD.exists()) {
			for (File f : IGMD.listFiles(new FileFilter() {
				@Override
				public boolean accept(File pathname) {
					return pathname.isDirectory();
				}
			})) {
				File ignore = CaseInsensitiveFile.getCaseInsensitiveFile(f,
						"ignore.txt");
				if (!ignore.exists()) {
					try {
						FileUtils.deleteDirectory(f);
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}

		TreeSet<Integer> unlockLevels = new TreeSet<Integer>();

		Vector<File> foldersOni = new Vector<File>();
		foldersOni.add(Paths.getVanillaOnisPath());

		Vector<File> foldersPatches = new Vector<File>();

		for (Package m : mods) {
			for (int lev : m.getUnlockLevels())
				unlockLevels.add(lev);

			File oni = CaseInsensitiveFile.getCaseInsensitiveFile(
					m.getLocalPath(), "oni");
			if (oni.exists()) {
				if (m.hasSeparatePlatformDirs()) {
					File oniCommon = CaseInsensitiveFile
							.getCaseInsensitiveFile(oni, "common");
					File oniMac = CaseInsensitiveFile.getCaseInsensitiveFile(
							oni, "mac_only");
					File oniWin = CaseInsensitiveFile.getCaseInsensitiveFile(
							oni, "win_only");
					if (oniCommon.exists())
						foldersOni.add(oniCommon);
					if (PlatformInformation.getPlatform() == Platform.MACOS
							&& oniMac.exists())
						foldersOni.add(oniMac);
					else if (oniWin.exists())
						foldersOni.add(oniWin);
				} else {
					foldersOni.add(oni);
				}
			}

			File patches = CaseInsensitiveFile.getCaseInsensitiveFile(
					m.getLocalPath(), "patches");
			if (patches.exists()) {
				if (m.hasSeparatePlatformDirs()) {
					File patchesCommon = CaseInsensitiveFile
							.getCaseInsensitiveFile(patches, "common");
					File patchesMac = CaseInsensitiveFile
							.getCaseInsensitiveFile(patches, "mac_only");
					File patchesWin = CaseInsensitiveFile
							.getCaseInsensitiveFile(patches, "win_only");
					if (patchesCommon.exists())
						foldersPatches.add(patchesCommon);
					if (PlatformInformation.getPlatform() == Platform.MACOS
							&& patchesMac.exists())
						foldersPatches.add(patchesMac);
					else if (patchesWin.exists())
						foldersPatches.add(patchesWin);
				} else {
					foldersPatches.add(patches);
				}
			}
		}

		TreeMap<String, Vector<File>> levels = new TreeMap<String, Vector<File>>(
				new NaturalOrderComparator());
		for (File path : foldersOni) {
			for (File levelF : path.listFiles()) {
				String fn = levelF.getName().toLowerCase();
				String levelN = null;
				if (levelF.isDirectory()) {
					levelN = fn;
					levelsAffectedNow.add(fn.toLowerCase());
				} else if (fn.endsWith(".dat")) {
					levelN = fn.substring(0, fn.lastIndexOf('.')).toLowerCase();
				}
				if (levelN != null) {
					if (!levels.containsKey(levelN))
						levels.put(levelN, new Vector<File>());
					levels.get(levelN).add(levelF);
				}
			}
		}

		Paths.getEditionGDF().mkdirs();
		for (File f : Paths.getEditionGDF().listFiles(new FilenameFilter() {
			public boolean accept(File arg0, String arg1) {
				String s = arg1.toLowerCase();
				return s.endsWith(".dat")
						|| s.endsWith(".raw")
						|| s.endsWith(".sep")
						|| (s.equals("intro.bik") && !SettingsManager
								.getInstance().get("copyintro", false))
						|| (s.equals("outro.bik") && !SettingsManager
								.getInstance().get("copyoutro", false));
			}
		})) {
			String l = f.getName().toLowerCase();
			l = l.substring(0, l.length() - 4);
			if ((levelsAffectedBefore == null)
					|| levelsAffectedBefore.contains(l)
					|| levelsAffectedNow.contains(l))
				f.delete();
		}

		applyPatches(levels, foldersPatches, listener, log);

		TreeSet<String> levelsAffectedBoth = null;
		if (levelsAffectedBefore != null) {
			levelsAffectedBoth = new TreeSet<String>();
			levelsAffectedBoth.addAll(levelsAffectedBefore);
			levelsAffectedBoth.addAll(levelsAffectedNow);
		}

		combineBinaryFiles(levels, levelsAffectedBoth, listener, log);
		combineBSLFolders(mods, listener, log);

		copyPlainFiles (log, mods, listener);
		
		copyVideos(log);

		if (unlockLevels.size() > 0) {
			unlockLevels(unlockLevels, log);
		}

		ModInstallationList mil = ModInstallationList.getInstance();
		mil.setAffectedLevels(levelsAffectedNow);
		TreeSet<Integer> modsInstalled = new TreeSet<Integer>();
		for (Package p : mods) {
			modsInstalled.add(p.getPackageNumber());
		}
		mil.setInstalledMods(modsInstalled);
		mil.saveList();

		log.println();
		Date end = new Date();
		log.println("Installation ended at " + sdf.format(end));
		log.println("Process took "
				+ ((end.getTime() - start.getTime()) / 1000) + " seconds");
		log.close();
	}

	private static void combineBSLFolders(TreeSet<Package> mods,
			InstallProgressListener listener, Logger log) {
		listener.installProgressUpdate(95, 100, AEInstaller2.globalBundle.getString("modInstaller.installBsl"));
		log.println();
		log.println("Installing BSL files");

		HashMap<EBSLInstallType, Vector<Package>> modsToInclude = new HashMap<EBSLInstallType, Vector<Package>>();
		modsToInclude.put(EBSLInstallType.NORMAL, new Vector<Package>());
		modsToInclude.put(EBSLInstallType.ADDON, new Vector<Package>());

		for (Package m : mods.descendingSet()) {
			File bsl = CaseInsensitiveFile.getCaseInsensitiveFile(
					m.getLocalPath(), "bsl");
			if (bsl.exists()) {
				if (m.hasSeparatePlatformDirs()) {
					File bslCommon = CaseInsensitiveFile
							.getCaseInsensitiveFile(bsl, "common");
					File bslMac = CaseInsensitiveFile.getCaseInsensitiveFile(
							bsl, "mac_only");
					File bslWin = CaseInsensitiveFile.getCaseInsensitiveFile(
							bsl, "win_only");
					if ((PlatformInformation.getPlatform() == Platform.MACOS && bslMac
							.exists())
							|| ((PlatformInformation.getPlatform() == Platform.WIN || PlatformInformation
									.getPlatform() == Platform.LINUX) && bslWin
									.exists()) || bslCommon.exists()) {
						modsToInclude.get(m.getBSLInstallType()).add(m);
					}
				} else {
					modsToInclude.get(m.getBSLInstallType()).add(m);
				}
			}
		}

		for (Package m : modsToInclude.get(EBSLInstallType.NORMAL)) {
			copyBSL(m, false, log);
		}
		Vector<Package> addons = modsToInclude.get(EBSLInstallType.ADDON);
		for (int i = addons.size() - 1; i >= 0; i--) {
			copyBSL(addons.get(i), true, log);
		}
	}

	private static void copyBSL(Package sourceMod, boolean addon, Logger log) {
		File targetBaseFolder = new File(Paths.getEditionGDF(), "IGMD");
		if (!targetBaseFolder.exists())
			targetBaseFolder.mkdir();

		Vector<File> sources = new Vector<File>();
		File bsl = CaseInsensitiveFile.getCaseInsensitiveFile(
				sourceMod.getLocalPath(), "bsl");
		if (sourceMod.hasSeparatePlatformDirs()) {
			File bslCommon = CaseInsensitiveFile.getCaseInsensitiveFile(bsl,
					"common");
			File bslMac = CaseInsensitiveFile.getCaseInsensitiveFile(bsl,
					"mac_only");
			File bslWin = CaseInsensitiveFile.getCaseInsensitiveFile(bsl,
					"win_only");
			if (PlatformInformation.getPlatform() == Platform.MACOS
					&& bslMac.exists()) {
				for (File f : bslMac.listFiles(dirFileFilter)) {
					File targetBSL = new File(targetBaseFolder, f.getName());
					if (addon || !targetBSL.exists())
						sources.add(f);
				}
			}
			if ((PlatformInformation.getPlatform() == Platform.WIN || PlatformInformation
					.getPlatform() == Platform.LINUX) && bslWin.exists()) {
				for (File f : bslWin.listFiles(dirFileFilter)) {
					File targetBSL = new File(targetBaseFolder, f.getName());
					if (addon || !targetBSL.exists())
						sources.add(f);
				}
			}
			if (bslCommon.exists()) {
				for (File f : bslCommon.listFiles(dirFileFilter)) {
					File targetBSL = new File(targetBaseFolder, f.getName());
					if (addon || !targetBSL.exists())
						sources.add(f);
				}
			}
		} else {
			for (File f : bsl.listFiles(dirFileFilter)) {
				File targetBSL = new File(targetBaseFolder, f.getName());
				if (addon || !targetBSL.exists())
					sources.add(f);
			}
		}

		log.println("\tMod \"" + sourceMod.getName() + "\"");
		for (File f : sources) {
			log.println("\t\t" + f.getName());
			File targetPath = new File(targetBaseFolder, f.getName());
			if (!targetPath.exists())
				targetPath.mkdir();
			if (!(CaseInsensitiveFile.getCaseInsensitiveFile(targetPath,
					"ignore.txt").exists())) {
				for (File fbsl : f.listFiles()) {
					if (fbsl.getName().toLowerCase().endsWith(".bsl")) {
						File targetFile = new File(targetPath, fbsl.getName());
						if (addon || !targetFile.exists()) {
							try {
								FileUtils.copyFile(fbsl, targetFile);
							} catch (IOException e) {
								e.printStackTrace();
							}
						}
					}
				}
			}
		}
	}

	private static void applyPatches(
			TreeMap<String, Vector<File>> oniLevelFolders,
			List<File> patchFolders, InstallProgressListener listener,
			Logger log) {
		log.println();
		log.println("Applying XML patches");
		listener.installProgressUpdate(0, 1, AEInstaller2.globalBundle.getString("modInstaller.applyXmlPatches"));

		long startMS = new Date().getTime();

		String tmpFolderName = "installrun_temp-"
				+ new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss")
						.format(new Date());
		File tmpFolder = new File(Paths.getTempPath(), tmpFolderName);
		tmpFolder.mkdir();

		TreeMap<String, Vector<File>> patches = new TreeMap<String, Vector<File>>(
				new NaturalOrderComparator());
		for (File patchFolder : patchFolders) {
			for (File levelFolder : patchFolder.listFiles(dirFileFilter)) {
				String lvlName = levelFolder.getName().toLowerCase();
				for (File f : FileUtils.listFiles(levelFolder,
						new String[] { "oni-patch" }, true)) {
					if (!patches.containsKey(lvlName))
						patches.put(lvlName, new Vector<File>());
					patches.get(lvlName).add(f);
				}
			}
		}

		for (String level : patches.keySet()) {
			File levelFolder = new File(tmpFolder, level);
			levelFolder.mkdir();

			log.println("\t\tPatches for " + level);

			Vector<String> exportPatterns = new Vector<String>();
			// Get files to be patched from vanilla.dat
			for (File patch : patches.get(level)) {
				String patternWildcard = patch.getName();
				patternWildcard = patternWildcard.substring(0,
						patternWildcard.indexOf(".oni-patch"));
				patternWildcard = patternWildcard.replace('-', '*');
				exportPatterns.add(patternWildcard);
			}
			for (File srcFolder : oniLevelFolders.get(level)) {
				if (srcFolder.isFile()) {
					if (srcFolder.getPath().toLowerCase().contains("vanilla")) {
						// Extract from .dat
						ApplicationInvocationResult res = OniSplit.export(
								levelFolder, srcFolder, exportPatterns);
						log.logAppOutput(res, true);
					}
				}
			}

			// Get files to be patched from packages
			for (File patch : patches.get(level)) {
				String patternWildcard = patch.getName();
				patternWildcard = patternWildcard.substring(0,
						patternWildcard.indexOf(".oni-patch"));
				patternWildcard = patternWildcard.replace('-', '*');
				patternWildcard = patternWildcard + ".oni";
				Vector<String> patterns = new Vector<String>();
				patterns.add(patternWildcard);
				final Pattern patternRegex = Pattern.compile(
						patternWildcard.replaceAll("\\*", ".\\*"),
						Pattern.CASE_INSENSITIVE);

				for (File srcFolder : oniLevelFolders.get(level)) {
					if (srcFolder.isFile()) {
						if (!srcFolder.getPath().toLowerCase()
								.contains("vanilla")) {
							// Extract from .dat
							ApplicationInvocationResult res = OniSplit.export(
									levelFolder, srcFolder, patterns);
							log.logAppOutput(res, true);
						}
					} else {
						// Copy from folder with overwrite
						for (File f : FileUtils.listFiles(srcFolder,
								new RegexFileFilter(patternRegex),
								TrueFileFilter.TRUE)) {
							try {
								FileUtils.copyFileToDirectory(f, levelFolder);
							} catch (IOException e) {
								e.printStackTrace();
							}
						}
					}
				}
			}

			// Extract files to XML
			File levelFolderXML = new File(levelFolder, "xml");
			Vector<File> files = new Vector<File>();
			files.add(new File(levelFolder, "*.oni"));
			ApplicationInvocationResult res = OniSplit.convertOniToXML(
					levelFolderXML, files);
			log.logAppOutput(res, true);

			// Create masterpatch file (containing calls to all individual
			// patches)
			File masterpatch = new File(levelFolderXML, "masterpatch.txt");
			PrintWriter masterpatchWriter = null;
			try {
				masterpatchWriter = new PrintWriter(new OutputStreamWriter(
						new FileOutputStream(masterpatch), "UTF-8"));
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			}
			for (File patch : patches.get(level)) {
				String patternWildcard = patch.getName();
				patternWildcard = patternWildcard.substring(0,
						patternWildcard.indexOf(".oni-patch"));
				patternWildcard = patternWildcard + ".xml";
				patternWildcard = patternWildcard.replace('-', '*');
				File xmlFilePath = new File(levelFolderXML, patternWildcard);
				masterpatchWriter.println(String.format("\"%s\" \"%s\"",
						patch.getPath(), xmlFilePath.getPath()));
			}
			masterpatchWriter.close();
			// Apply patches through masterpatch in levelFolderXML
			res = XMLTools.patch(masterpatch);
			log.logAppOutput(res, true);

			// Create .oni files from XML
			files.clear();
			files.add(new File(levelFolderXML, "*.xml"));
			res = OniSplit.convertXMLtoOni(levelFolder, files);
			log.logAppOutput(res, true);

			if (!RuntimeOptions.isDebug()) {
				// Remove XML folder as import will only require .oni's
				try {
					FileUtils.deleteDirectory(levelFolderXML);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}

			oniLevelFolders.get(level).add(levelFolder);
		}

		log.println("Applying XML patches took "
				+ (new Date().getTime() - startMS) + " ms");
	}

	private static void combineBinaryFiles(
			TreeMap<String, Vector<File>> oniLevelFolders,
			TreeSet<String> levelsUpdated, InstallProgressListener listener,
			Logger log) {
		long startMS = new Date().getTime();

		int totalSteps = oniLevelFolders.size() + 1;
		int stepsDone = 0;

		log.println();
		log.println("Importing levels");
		for (String l : oniLevelFolders.keySet()) {
			log.println("\tLevel " + l);
			listener.installProgressUpdate(stepsDone, totalSteps,
					AEInstaller2.globalBundle.getString("modInstaller.buildingLevelN").replaceAll("%1", l.toString()));

			if ((levelsUpdated == null)
					|| levelsUpdated.contains(l.toLowerCase())) {
				ApplicationInvocationResult res = OniSplit.packLevel(
						oniLevelFolders.get(l), new File(Paths.getEditionGDF(),
								sanitizeLevelName(l) + ".dat"));
				log.logAppOutput(res, true);
			} else {
				log.println("\t\tLevel not affected by new mod selection");
				log.println();
			}

			stepsDone++;
		}

		log.println("Importing levels took " + (new Date().getTime() - startMS)
				+ " ms");
		log.println();
	}

	private static void copyVideos(Logger log) {
		log.println();
		if (SettingsManager.getInstance().get("copyintro", false)) {
			File src = new File(Paths.getVanillaGDF(), "intro.bik");
			File target = new File(Paths.getEditionGDF(), "intro.bik");
			log.println("Copying intro");
			if (src.exists() && !target.exists()) {
				try {
					FileUtils.copyFileToDirectory(src, Paths.getEditionGDF());
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		} else {
			log.println("NOT copying intro");
		}
		if (SettingsManager.getInstance().get("copyoutro", true)) {
			File src = new File(Paths.getVanillaGDF(), "outro.bik");
			File target = new File(Paths.getEditionGDF(), "outro.bik");
			log.println("Copying outro");
			if (src.exists() && !target.exists()) {
				try {
					FileUtils.copyFileToDirectory(src, Paths.getEditionGDF());
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		} else {
			log.println("NOT copying outro");
		}
	}

	private static void copyPlainFiles(final Logger log, TreeSet<Package> mods, InstallProgressListener listener) {
		listener.installProgressUpdate(97, 100, AEInstaller2.globalBundle.getString("modInstaller.copyPlainFiles"));
		log.println();
		log.println("Copying plain files from mods");

		for (Package p : mods) {
			ToolFileIterator.iteratePlatformToolFiles(p,
					new ToolFileIteratorEntry() {
						@Override
						public void toolFile(File source, File target, boolean isDir) {
							copyPlainFile(source, target, log);
						}
					});
		}
	}

	private static void copyPlainFile(File src, File target, Logger log) {
		try {
			if (src.getAbsolutePath().toLowerCase().contains("gamedatafolder")) {
				File targetFile = CaseInsensitiveFile.getCaseInsensitiveFile(
						target.getParentFile(), target.getName());

				// Case mismatch?
				if (!targetFile.getName().equals(src.getName()))
					targetFile.delete();

				FileUtils.copyFile(src, target);
			} else {
				log.printlnFmt("Not copying \"%s\": Not within GameDataFolder", src.getPath());
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}


	private static void unlockLevels(TreeSet<Integer> unlockLevels, Logger log) {
		File dat = new File(Paths.getEditionBasePath(), "persist.dat");
		log.println();
		log.println("Unlocking levels: " + unlockLevels.toString());
		if (!dat.exists()) {
			InputStream is = AEInstaller2.class
					.getResourceAsStream("/net/oni2/aeinstaller/resources/persist.dat");
			try {
				FileUtils.copyInputStreamToFile(is, dat);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		PersistDat save = new PersistDat(dat);
		HashSet<Integer> currentlyUnlocked = save.getUnlockedLevels();
		currentlyUnlocked.addAll(unlockLevels);
		save.setUnlockedLevels(currentlyUnlocked);
		save.close();
	}

	private static String sanitizeLevelName(String ln) {
		int ind = ln.indexOf("_");
		String res = ln.substring(0, ind + 1);
		res += ln.substring(ind + 1, ind + 2).toUpperCase();
		res += ln.substring(ind + 2);
		return res;
	}

}
