/*
 *  Copyright (C) 2006-2021  Ronald Blankendaal
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */
package exodos;

import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.dbgl.gui.interfaces.ProgressNotifyable;
import org.dbgl.model.FileLocation;
import org.dbgl.model.GamePack;
import org.dbgl.model.Link;
import org.dbgl.model.SearchResult;
import org.dbgl.model.SearchResult.ResultType;
import org.dbgl.model.aggregate.DosboxVersion;
import org.dbgl.model.aggregate.Profile;
import org.dbgl.model.conf.Autoexec;
import org.dbgl.model.conf.Settings;
import org.dbgl.model.conf.mount.DirMount;
import org.dbgl.model.conf.mount.ImageMount;
import org.dbgl.model.conf.mount.Mount;
import org.dbgl.model.entity.GamePackEntry;
import org.dbgl.model.factory.ProfileFactory;
import org.dbgl.model.repository.DosboxVersionRepository;
import org.dbgl.service.FileLocationService;
import org.dbgl.service.ITextService;
import org.dbgl.service.ImportExportProfilesService;
import org.dbgl.service.TextService;
import org.dbgl.util.FilesUtils;
import org.dbgl.util.ShortFilenameUtils;
import org.dbgl.util.StringRelatedUtils;
import org.dbgl.util.XmlUtils;
import org.dbgl.util.archive.ZipUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


public class Convert {

	private static final String DOS_METADATA_ARC = "!DOSmetadata.zip";
	private static final String XODOS_METADATA_ARC = "XODOSMetadata.zip";
	private static final String XODOS_METADATA_XML = "xml" + File.separator + "MS-DOS.xml";
	private static final File MAIN_METADATA_ARC_GAMEDIR = new File("Games", "!dos");
	private static final File MAIN_METADATA_IMGDIR = new File("Images", "MS-DOS");
	private static final List<String> GAME_ARCS = Arrays.asList("GamesSTR.zip", "GamesSIM.zip", "GamesRPG.zip", "GamesADV.zip", "GamesACT.zip", "GamesWIN.zip", "Games.zip");

	private static final String EXODOS_DIR = "eXoDOS";
	private static final String GAMES_DIR = "games";
	private static final String UTIL_DIR = "util";

	private static final String MEAGRE_DIR = "Meagre";
	private static final String INIFILE_DIR = "IniFile";
	private static final String ABOUT_DIR = "About";

	private static final String EXTRAS_DIR = "Extras";
	private static final String MANUAL_DIR = "Manual";

	private static final String FRONT_DIR = "Front";
	private static final String BACK_DIR = "Back";
	private static final String MEDIA_DIR = "Media";
	private static final String ADVERT_DIR = "Advert";
	private static final String TITLE_DIR = "Title";
	private static final String SCREEN_DIR = "Screen";
	private static final String[] CAP_DIRS = {TITLE_DIR, SCREEN_DIR, FRONT_DIR, BACK_DIR, MEDIA_DIR, ADVERT_DIR};

	private static final String GPA_TITLE = "eXoDOS conversion";
	private static final String GPA_NOTES = StringUtils.EMPTY;
	private static final String GPA_AUTHOR = StringUtils.EMPTY;

	private static final String CONVERTER_TITLE = "eXoDOS converter";
	private static final String CONVERTER_VERSION = "0.93";

	private static final long BYTES_IN_MB = 1024L * 1024L;
	private static final long MAX_PART_SIZE_DEFAULT_IN_MB = 1024L * 16L;
	private static final String[] CDIMAGES = {".iso", ".cue", ".bin", ".img", ".gog"};
	private static final String[] EXTRAFILES = {"rtf", "pdf", "doc", "xls", "htm", "html", "txt", "jpg", "png", "bmp", "gif", "flac", "wav", "ogg", "mp2", "mp3", "mp4"};

	private static final Pattern IMG_FILENAME_PTRN = Pattern.compile("^(.*?)(\\.........\\-....\\-....\\-....\\-............)?\\-[0-9][0-9]( \\(.\\))?\\.(.*)$");

	private static final ITextService TEXT = TextService.getInstance();

	private static final String EXODOS_V5_CONTENT_DIR = "Content";
	private static final String EXODOS_V5_EXO_DIR = "eXo";
	private static final String EXODOS_V5_GAMEZIPS_DIR = EXODOS_V5_EXO_DIR + File.separator + EXODOS_DIR;

	private static final Charset CP437 = Charset.forName("CP437");

	private static boolean verboseOutput_ = false;
	private static long maxPartSizeInMB_ = MAX_PART_SIZE_DEFAULT_IN_MB;
	private static int nrOfThreads_ = Math.min(Runtime.getRuntime().availableProcessors(), 6);

	public static void main(String[] args) {
		System.out.println("Converts eXoDOS game packages into DBGL GamePackArchives (v" + CONVERTER_VERSION + ")\n");

		if (args.length < 2 || args.length > 6)
			displaySyntax();

		File inputDir = new File(args[0]);
		File tmpDir = new File(args[1]);

		boolean analyzeOnly = false;
		boolean keepExtractedMetaData = false;

		if (args.length > 2) {
			for (int i = 2; i < args.length; i++) {
				if (args[i].equalsIgnoreCase("-a"))
					analyzeOnly = true;
				else if (args[i].equalsIgnoreCase("-k"))
					keepExtractedMetaData = true;
				else if (args[i].equalsIgnoreCase("-v"))
					verboseOutput_ = true;
				else if (args[i].toLowerCase().startsWith("-s:")) {
					try {
						maxPartSizeInMB_ = Long.parseLong(args[i].substring(3));
					} catch (@SuppressWarnings("unused") NumberFormatException e) {
						// ignore, use the default value
					}
				} else if (args[i].toLowerCase().startsWith("-t:")) {
					try {
						nrOfThreads_ = Integer.parseInt(args[i].substring(3));
					} catch (@SuppressWarnings("unused") NumberFormatException e) {
						// ignore, use the default value
					}
				} else
					displaySyntax();
			}
		}

		if (analyzeOnly)
			System.out.println("* Analyze only");
		if (keepExtractedMetaData)
			System.out.println("* Keeping extracted data after processing");
		if (verboseOutput_)
			System.out.println("* Verbose output");
		if (maxPartSizeInMB_ != MAX_PART_SIZE_DEFAULT_IN_MB)
			System.out.println("* Target size of the GamePackArchives: " + maxPartSizeInMB_ + "MB");

		if (analyzeOnly || keepExtractedMetaData || verboseOutput_ || (maxPartSizeInMB_ != MAX_PART_SIZE_DEFAULT_IN_MB))
			System.out.println();

		List<DosboxVersion> dbversionsList = null;
		try {

			DosboxVersionRepository dosboxRepo = new DosboxVersionRepository();
			dbversionsList = dosboxRepo.listAll();

			if (DosboxVersionRepository.findDefault(dbversionsList) == null) {
				SearchResult result = FileLocationService.getInstance().findDosbox();
				if (result.result_ == ResultType.COMPLETE) {
					new DosboxVersionRepository().add(result.dosbox_);
					dbversionsList = dosboxRepo.listAll();
				}
				if (DosboxVersionRepository.findDefault(dbversionsList) == null) {
					System.out.println("DOSBox installation could not be located, exiting.");
					System.exit(1);
				}
			}

			if (verboseOutput_)
				System.out.println("Using DOSBox installation located in: [" + DosboxVersionRepository.findDefault(dbversionsList).getConfigurationCanonicalFile().getPath() + "]");
		} catch (SQLException e) {
			e.printStackTrace();
			System.exit(1);
		}

		DosboxVersion defaultDosboxVersion = DosboxVersionRepository.findDefault(dbversionsList);

		if (validateExoV5Parameters(inputDir, tmpDir)) {

			System.out.println("eXoDOS V5 found");

			File contentDir = new File(inputDir, EXODOS_V5_CONTENT_DIR);
			File dosMetadataFile = new File(contentDir, DOS_METADATA_ARC);
			File xodosMetadataFile = new File(contentDir, XODOS_METADATA_ARC);
			File gameZipsDir = new File(inputDir, EXODOS_V5_GAMEZIPS_DIR);

			File tmpDirForXodosZip = new File(tmpDir, FilenameUtils.getBaseName(xodosMetadataFile.getName()));
			File tmpDirForDosZip = new File(tmpDir, FilenameUtils.getBaseName(dosMetadataFile.getName()));

			extractV5MetaData(inputDir, xodosMetadataFile, tmpDirForXodosZip, dosMetadataFile, tmpDirForDosZip);

			GamePack gamePack = analyzeV5MetaData(defaultDosboxVersion, gameZipsDir, tmpDirForXodosZip, tmpDirForDosZip);

			if (!analyzeOnly)
				generateGamePackArchives(gamePack, tmpDir, "eXoV5");

			if (!keepExtractedMetaData) {
				cleanup(tmpDirForXodosZip);
				cleanup(tmpDirForDosZip);
			}

		} else {

			System.out.println("eXoDOS V5 not found, looking for V4...");

			List<File> inputGamesZips = validateExoV4Parameters(inputDir, tmpDir);
			File gamesDir = new File(new File(inputDir, EXODOS_DIR), GAMES_DIR);

			for (File inputGamesZip: inputGamesZips) {
				File tmpDirForGameZip = new File(tmpDir, FilenameUtils.getBaseName(inputGamesZip.getName()));

				File gamesGamesDir = extractMeagreMetaData(inputGamesZip, tmpDirForGameZip);
				File xodosTmpDir = null;

				File xodosMetadataFile = new File(inputDir, XODOS_METADATA_ARC);
				if (FilesUtils.isExistingFile(xodosMetadataFile)) {
					File tmpDirForZip = new File(tmpDir, FilenameUtils.getBaseName(xodosMetadataFile.getName()));
					xodosTmpDir = extractLaunchBoxMetaData(true, xodosMetadataFile, tmpDirForZip);
				}

				GamePack gamePack = analyzeMeagreMetaData(gamesDir, gamesGamesDir, defaultDosboxVersion);

				if (FilesUtils.isExistingFile(xodosMetadataFile))
					gamePack = analyzeLaunchBoxMetaData(gamesGamesDir, xodosTmpDir, gamePack);

				if (!analyzeOnly)
					generateGamePackArchives(gamePack, tmpDir, FilenameUtils.removeExtension(inputGamesZip.getName()));

				if (!keepExtractedMetaData) {
					cleanup(tmpDirForGameZip);
					if (FilesUtils.isExistingFile(xodosMetadataFile)) {
						cleanup(xodosTmpDir);
					}
				}
			}

		}
	}

	private static void displaySyntax() {
		System.out.println("Use: Convert <inputexodosdir> <dstdir> [-a] [-k] [-v] [-s:size]");
		System.out.println("-a\t\tAnalyze only, don't generate GamePackArchives");
		System.out.println("-k\t\tKeep extracted meta data files after processing");
		System.out.println("-v\t\tVerbose output");
		System.out.println("-s:size\t\tTarget size of the GamePackArchives in MB, " + MAX_PART_SIZE_DEFAULT_IN_MB + " is the default (= " + MAX_PART_SIZE_DEFAULT_IN_MB / 1024L + " GB packages)");
		System.out.println("-t:nrOfThreads\t\tAmount of CPU cores to use when generating GamePackArchives (instead of the default " + nrOfThreads_ + " threads)");
		System.exit(1);
	}

	private static List<File> validateExoV4Parameters(File inputDir, File tmpDir) {
		if (!inputDir.exists()) {
			System.out.println("The directory [" + inputDir + "] does not exist, exiting.");
			System.exit(1);
		}
		File exodosDir = new File(inputDir, EXODOS_DIR);
		if (!exodosDir.exists()) {
			System.out.println("The directory [" + inputDir + "] does not contain the [" + EXODOS_DIR + "] directory, exiting.");
			System.exit(1);
		}
		File gamesDir = new File(exodosDir, GAMES_DIR);
		if (!gamesDir.exists()) {
			System.out.println("The directory [" + exodosDir + "] does not contain the [" + GAMES_DIR + "] directory, exiting.");
			System.exit(1);
		}
		File utilDir = new File(exodosDir, UTIL_DIR);
		if (!utilDir.exists()) {
			System.out.println("The directory [" + exodosDir + "] does not contain the [" + UTIL_DIR + "] directory, exiting.");
			System.exit(1);
		}

		List<File> inputGamesZip = new ArrayList<>();
		File mainFile = new File(inputDir, DOS_METADATA_ARC);
		if (FilesUtils.isExistingFile(mainFile))
			inputGamesZip.add(mainFile);

		for (String filename: GAME_ARCS) {
			File file = new File(gamesDir, filename);
			if (FilesUtils.isExistingFile(file))
				inputGamesZip.add(file);
		}

		if (inputGamesZip.isEmpty()) {
			System.out.println("None of the files [" + StringUtils.join(GAME_ARCS, ", ") + "] are found, exiting.");
			System.exit(1);
		}
		if (!tmpDir.exists()) {
			System.out.println("The directory [" + tmpDir + "] does not exist, exiting.");
			System.exit(1);
		}
		return inputGamesZip;
	}

	private static boolean validateExoV5Parameters(File inputDir, File tmpDir) {
		if (!inputDir.exists()) {
			System.out.println("The directory [" + inputDir + "] does not exist.");
			return false;
		}
		File contentDir = new File(inputDir, EXODOS_V5_CONTENT_DIR);
		if (!contentDir.exists()) {
			System.out.println("The directory [" + inputDir + "] does not contain the [" + EXODOS_V5_CONTENT_DIR + "] directory.");
			return false;
		}
		File exoDir = new File(inputDir, EXODOS_V5_EXO_DIR);
		if (!exoDir.exists()) {
			System.out.println("The directory [" + inputDir + "] does not contain the [" + EXODOS_V5_EXO_DIR + "] directory.");
			return false;
		}
		File gameZipsDir = new File(exoDir, EXODOS_DIR);
		if (!gameZipsDir.exists()) {
			System.out.println("The directory [" + exoDir + "] does not contain the [" + EXODOS_DIR + "] directory.");
			return false;
		}
		File utilDir = new File(exoDir, UTIL_DIR);
		if (!utilDir.exists()) {
			System.out.println("The directory [" + exoDir + "] does not contain the [" + UTIL_DIR + "] directory.");
			return false;
		}
		File dosMetadataFile = new File(contentDir, DOS_METADATA_ARC);
		if (!FilesUtils.isExistingFile(dosMetadataFile)) {
			System.out.println("The file [" + dosMetadataFile + "] does not exist.");
			return false;
		}
		File xodosMetadataFile = new File(contentDir, XODOS_METADATA_ARC);
		if (!FilesUtils.isExistingFile(xodosMetadataFile)) {
			System.out.println("The file [" + xodosMetadataFile + "] does not exist.");
			return false;
		}
		if (!tmpDir.exists()) {
			System.out.println("The directory [" + tmpDir + "] does not exist.");
			return false;
		}
		return true;
	}

	private static File extractMeagreMetaData(File inputGamesZip, File tmpDir) {
		System.out.println();
		System.out.println("===========================================");
		System.out.println(" Phase 1 of 3: Extracting Meagre meta-data");
		System.out.println("===========================================");
		System.out.println("Reading from: [" + inputGamesZip + "]");
		System.out.println("Writing to:   [" + tmpDir + "]");

		try {
			String mainGamesDir = findMainFolder(inputGamesZip);
			if (mainGamesDir == null) {
				System.out.println("The file [" + inputGamesZip + "] does not seem to have an inner games directory, exiting.");
				System.exit(1);
			}
			File gamesGamesDir = new File(tmpDir, mainGamesDir);
			if (!gamesGamesDir.exists()) {
				unzip(inputGamesZip, tmpDir);
			} else {
				System.out.println("Skipping extraction of [" + inputGamesZip + "] since [" + gamesGamesDir + "] already exists");
			}
			File metaDataGameDir = new File(gamesGamesDir, MAIN_METADATA_ARC_GAMEDIR.getPath());
			return metaDataGameDir.exists() ? metaDataGameDir: gamesGamesDir;
		} catch (IOException e) {
			System.out.println("The file [" + inputGamesZip + "] did not fully extract into the [" + tmpDir + "] directory, exiting.");
			e.printStackTrace();
			System.exit(1);
		}
		return null;
	}

	private static File extractLaunchBoxMetaData(boolean intro, File inputZip, File tmpDir) {
		if (intro) {
			System.out.println();
			System.out.println("===========================================");
			System.out.println(" Now extracting LaunchBox meta-data");
			System.out.println("===========================================");
			System.out.println("Reading from: [" + inputZip + "]");
			System.out.println("Writing to:   [" + tmpDir + "]");
		}

		try {
			if (!tmpDir.exists()) {
				unzip(inputZip, tmpDir);
			} else {
				System.out.println("Skipping extraction of [" + inputZip + "] since [" + tmpDir + "] already exists");
			}
			return tmpDir;
		} catch (IOException e) {
			System.out.println("The file [" + inputZip + "] did not fully extract into the [" + tmpDir + "] directory, exiting.");
			e.printStackTrace();
			System.exit(1);
		}
		return null;
	}

	private static GamePack analyzeMeagreMetaData(File gamesDir, File gamesGamesDir, DosboxVersion defaultDosboxVersion) {
		System.out.println();
		System.out.println("==========================================");
		System.out.println(" Phase 2 of 3: Analyzing Meagre meta-data");
		System.out.println("==========================================");
		System.out.println("Reading from: [" + gamesGamesDir + "]");

		GamePack gamePack = new GamePack();
		gamePack.setCreationApp(CONVERTER_TITLE);
		gamePack.setCreationAppVersion(CONVERTER_VERSION);
		gamePack.setCreationDate(new Date());
		gamePack.setTitle(GPA_TITLE);
		gamePack.setAuthor(GPA_AUTHOR);
		gamePack.setNotes(GPA_NOTES);
		gamePack.setCapturesAvailable(true);
		gamePack.setGamedataAvailable(true);
		gamePack.setMapperfilesAvailable(false);
		gamePack.setNativecommandsAvailable(false);
		gamePack.setVersion(ImportExportProfilesService.PROFILES_XML_FORMAT_VERSION);
		gamePack.setDosboxVersions(Collections.singleton(defaultDosboxVersion));

		File[] gameDirs = gamesGamesDir.listFiles(new FileFilter() {
			@Override
			public boolean accept(File file) {
				return file.isDirectory();
			}
		});
		Arrays.sort(gameDirs, new Comparator<File>() {
			@Override
			public int compare(File file1, File file2) {
				return file1.getPath().compareToIgnoreCase(file2.getPath());
			}
		});

		for (int i = 0; i < gameDirs.length; i++) {

			File gameDir = gameDirs[i];
			File meagreDir = new File(gameDir, MEAGRE_DIR);
			File iniFileDir = new File(meagreDir, INIFILE_DIR);

			String gameDirName = gameDir.getName();

			File[] iniFiles = iniFileDir.listFiles(new FileFilter() {
				@Override
				public boolean accept(File file) {
					return file.isFile() && file.getName().toLowerCase().endsWith(".ini");
				}
			});
			if (iniFiles.length != 1) {
				System.out.println("WARNING: " + gameDirName + ": Not exactly 1 ini file found (" + iniFiles.length + ")");
			}
			File iniFile = iniFiles.length > 0 ? iniFiles[0]: null;

			try {

				Profile profile = null;
				File zipFile = null;

				boolean favorite = false;
				String confPathAndFile = new File(gameDir, FileLocationService.DOSBOX_CONF_STRING).getPath();

				List<File> capFiles = new ArrayList<>();
				LinkedHashMap<Long, File> extraFiles = new LinkedHashMap<>();
				List<File> extraDstFiles = new ArrayList<>();
				String[] links = new String[Profile.NR_OF_LINK_DESTINATIONS];
				Arrays.fill(links, StringUtils.EMPTY);
				String[] linkTitles = new String[Profile.NR_OF_LINK_TITLES];
				Arrays.fill(linkTitles, StringUtils.EMPTY);

				if (iniFile != null) {

					Settings iniConf = new Settings();
					iniConf.setFileLocation(new FileLocation(iniFile.getPath()));
					System.out.print(iniConf.load(TEXT));

					String title = iniConf.getValue("Main", "name", StringUtils.EMPTY);
					String developer = StringUtils.join(new String[] {iniConf.getValue("Main", "developer", StringUtils.EMPTY), iniConf.getValue("Main", "designer", StringUtils.EMPTY),
							iniConf.getValue("Main", "designer2", StringUtils.EMPTY)},
						", ");
					String publisher = iniConf.getValue("Main", "publisher", StringUtils.EMPTY);
					String genre = StringUtils.join(new String[] {iniConf.getValue("Main", "genre", StringUtils.EMPTY), iniConf.getValue("Main", "subgenre", StringUtils.EMPTY),
							iniConf.getValue("Main", "subgenre2", StringUtils.EMPTY)},
						", ");
					String year = iniConf.getValue("Main", "year", StringUtils.EMPTY);
					String status = StringUtils.EMPTY;
					String aboutFilename = iniConf.getValue("Main", "about", StringUtils.EMPTY);
					String notes = StringUtils.isBlank(aboutFilename) ? StringUtils.EMPTY
							: org.apache.commons.io.FileUtils.readFileToString(new File(meagreDir, ABOUT_DIR + File.separatorChar + aboutFilename), Charset.defaultCharset());

					List<String> linksList = new ArrayList<>();
					List<String> linkTitlesList = new ArrayList<>();
					addLink(iniConf.getValue("Main", "extra1", StringUtils.EMPTY), iniConf.getValue("Main", "extralink1", StringUtils.EMPTY), extraFiles, extraDstFiles, linkTitlesList, linksList,
						false, meagreDir, gameDirName);
					addLink("Manual", iniConf.getValue("Main", "manual", StringUtils.EMPTY), extraFiles, extraDstFiles, linkTitlesList, linksList, true, meagreDir, gameDirName);
					addLink(iniConf.getValue("Main", "extra2", StringUtils.EMPTY), iniConf.getValue("Main", "extralink2", StringUtils.EMPTY), extraFiles, extraDstFiles, linkTitlesList, linksList,
						false, meagreDir, gameDirName);
					addLink(iniConf.getValue("Main", "extra3", StringUtils.EMPTY), iniConf.getValue("Main", "extralink3", StringUtils.EMPTY), extraFiles, extraDstFiles, linkTitlesList, linksList,
						false, meagreDir, gameDirName);
					addLink(iniConf.getValue("Main", "extra4", StringUtils.EMPTY), iniConf.getValue("Main", "extralink4", StringUtils.EMPTY), extraFiles, extraDstFiles, linkTitlesList, linksList,
						false, meagreDir, gameDirName);
					addLink(iniConf.getValue("Main", "extra5", StringUtils.EMPTY), iniConf.getValue("Main", "extralink5", StringUtils.EMPTY), extraFiles, extraDstFiles, linkTitlesList, linksList,
						false, meagreDir, gameDirName);
					addLink(iniConf.getValue("Main", "extra6", StringUtils.EMPTY), iniConf.getValue("Main", "extralink6", StringUtils.EMPTY), extraFiles, extraDstFiles, linkTitlesList, linksList,
						false, meagreDir, gameDirName);
					addLink(iniConf.getValue("Main", "extra7", StringUtils.EMPTY), iniConf.getValue("Main", "extralink7", StringUtils.EMPTY), extraFiles, extraDstFiles, linkTitlesList, linksList,
						false, meagreDir, gameDirName);

					String[] capFilenames = {iniConf.getValue("Main", "title01", StringUtils.EMPTY), iniConf.getValue("Main", "screen01", StringUtils.EMPTY),
							iniConf.getValue("Main", "front01", StringUtils.EMPTY), iniConf.getValue("Main", "back01", StringUtils.EMPTY), iniConf.getValue("Main", "media01", StringUtils.EMPTY),
							iniConf.getValue("Main", "adv01", StringUtils.EMPTY)};

					for (int c = 0; c < capFilenames.length; c++) {
						if (StringUtils.isNotEmpty(capFilenames[c])) {
							File capFile = new File(meagreDir, CAP_DIRS[c] + File.separatorChar + capFilenames[c]);
							if (FilesUtils.isExistingFile(capFile))
								capFiles.add(capFile);
							else if (verboseOutput_)
								System.out.println(gameDirName + ": capture [" + capFile + "] not found, skipped");
						}
					}

					File[] extras = new File(meagreDir, EXTRAS_DIR).listFiles(new FileFilter() {
						@Override
						public boolean accept(File file) {
							return file.isFile() && Stream.of(EXTRAFILES).anyMatch(x -> x.equalsIgnoreCase(FilenameUtils.getExtension(file.getName())));
						}
					});
					for (File extraFile: extras)
						addLink(StringUtils.EMPTY, extraFile.getName(), extraFiles, extraDstFiles, linkTitlesList, linksList, false, meagreDir, gameDirName);

					for (int j = 0; j < Math.min(links.length, linksList.size()); j++) {
						links[j] = linksList.get(j);
						linkTitles[j] = linkTitlesList.get(j);
					}

					profile = ProfileFactory.create(i, title, developer, publisher, genre, year, status, notes, favorite, links, linkTitles, defaultDosboxVersion, confPathAndFile);

					zipFile = new File(gamesDir, FilenameUtils.getBaseName(iniConf.getValue("Main", "executable", StringUtils.EMPTY)) + ".zip");

				} else {

					profile = ProfileFactory.create(i, gameDirName, StringUtils.EMPTY, StringUtils.EMPTY, StringUtils.EMPTY, StringUtils.EMPTY, StringUtils.EMPTY, StringUtils.EMPTY, favorite, links,
						links, defaultDosboxVersion, confPathAndFile);

				}

				System.out.print(profile.resetAndLoadConfiguration());

				// minor bit of clean up of the actual dosbox configuration
				if ("64".equals(profile.getConfiguration().getValue("dosbox", "memsize")))
					profile.getConfiguration().setValue("dosbox", "memsize", "63");
				if ("overlay".equals(profile.getConfiguration().getValue("sdl", "output")))
					profile.getConfiguration().removeValue("sdl", "output");

				Autoexec autoexec = profile.getConfiguration().getAutoexec();
				autoexec.migrate(new FileLocation(GAMES_DIR, FileLocationService.getInstance().dosrootRelative()), FileLocationService.getInstance().getDosrootLocation());

				// Some extra sanity-checking on install.bat
				File gameFolderInstallbat = null, zipFileInstallbat = null;

				File installFile = new File(gameDir, "Install.bat");
				if (FilesUtils.isExistingFile(installFile)) {
					List<String> lines = org.apache.commons.io.FileUtils.readLines(installFile, Charset.defaultCharset());
					for (String s: lines) {
						if (s.startsWith("IF EXIST \"")) {
							int secondQuotes = s.lastIndexOf('\"');
							if (secondQuotes != -1) {
								gameFolderInstallbat = new File(s.substring(10, secondQuotes));
								if (!gameFolderInstallbat.getName().equalsIgnoreCase(gameDirName))
									System.out.println("WARNING: " + gameDirName
											+ ": This game's folder as found in games???.zip does not match the folder being checked for existence in install.bat (" + gameFolderInstallbat + ")");
							}
						} else if (s.startsWith("unzip \"")) {
							int secondQuotes = s.lastIndexOf('\"');
							if (secondQuotes != -1) {
								zipFileInstallbat = new File(gamesDir, s.substring(7, secondQuotes));
								if (!zipFileInstallbat.equals(zipFile) && verboseOutput_)
									System.out.println(gameDirName + ": This game's 'Main Executable' referenced in iniFile (" + zipFile + ") does not match the file being unzipped in install.bat ("
											+ zipFileInstallbat + ")");
							}
						}
					}
				} else {
					System.out.println("WARNING: " + gameDirName + ": This game's install.bat not found");
				}

				if (!FilesUtils.isExistingFile(zipFile)) {
					if ((zipFileInstallbat != null) && FilesUtils.isExistingFile(zipFileInstallbat)) {
						if (verboseOutput_)
							System.out.println("Zip file " + zipFile + " is missing but " + zipFileInstallbat + " does exist, using that file instead");
						zipFile = zipFileInstallbat;
					} else {
						if (gameDirName.equals("IBMMJ")) {
							zipFile = new File(gamesDir, "Mahjong (1986).zip");
							gameFolderInstallbat = new File("Mahjng86");
							autoexec.setGameMainPath(gameFolderInstallbat);
						} else if (gameDirName.equals("ANightma")) {
							zipFile = new File(gamesDir, "Nightmare on Elm Street, A (1989).zip");
						} else if (gameDirName.equals("BluBro")) {
							zipFile = new File(gamesDir, "Blues Brothers, The (1991).zip");
						} else if (gameDirName.equals("JimPower")) {
							zipFile = new File(gamesDir, "Jim Power - The Lost Dimension in 3D (1993).zip.zip");
						} else if (gameDirName.equals("RadixBey")) {
							zipFile = new File(gamesDir, "Radix - Beyond the Void (1995).zip");
						} else if (gameDirName.equals("SidLin96")) {
							zipFile = new File(gamesDir, "SideLine (1996).zip");
						} else if (gameDirName.equals("Sinaria")) {
							zipFile = new File(gamesDir, "Sinaria - Lost in Space (1994).zip");
						} else if (gameDirName.equals("Targ1982")) {
							zipFile = new File(gamesDir, "Target (IBM)(1982).zip");
						} else if (gameDirName.equals("bio101")) {
							zipFile = new File(gamesDir, "Biology 101 (1987).zip");
						} else if (gameDirName.equals("TheWorl")) {
							zipFile = new File(gamesDir, "World Name Game, The (1989).zip");
						} else if (gameDirName.equals("TNFS")) {
							zipFile = new File(gamesDir, "Need for Speed, The (1995).zip");
						} else if (gameDirName.equals("AFAB1985")) {
							zipFile = new File(gamesDir, "Fable, A (1985).zip");
						} else if (gameDirName.equals("shogun86")) {
							zipFile = new File(gamesDir, "James Clavell's Shogun (1986).zip");
						}
					}
				}

				List<File> foldersInZip = readFoldersInZip(zipFile);
				if (gameFolderInstallbat != null && !foldersInZip.contains(new File(gameFolderInstallbat.getName()))) {
					if (zipFileInstallbat != null && FilesUtils.isExistingFile(zipFileInstallbat)) {
						List<File> foldersInZipFileInstallBat = readFoldersInZip(zipFileInstallbat);
						if (foldersInZipFileInstallBat.contains(new File(gameFolderInstallbat.getName()))) {
							zipFile = zipFileInstallbat;
							if (verboseOutput_)
								System.out.println("Zip file " + zipFile + " contains the game folder [\" + gameFolderInstallbat.getName() + \"], using that file instead");
						} else {
							System.out.println("WARNING: " + gameDirName + ": Game folder [" + gameFolderInstallbat.getName() + "] not found inside [" + zipFile + "] or [" + zipFileInstallbat + "]");
						}
					} else {
						System.out.println("WARNING: " + gameDirName + ": Game folder [" + gameFolderInstallbat.getName() + "] not found inside [" + zipFile + "]");
					}
				}

				if (!FilesUtils.isExistingFile(zipFile)) {
					if (verboseOutput_)
						System.out.println("Zip file " + zipFile + " not found, reverting to Install.bat to determine game zip");
					if (zipFileInstallbat != null && FilesUtils.isExistingFile(zipFileInstallbat)) {
						zipFile = zipFileInstallbat;
						if (verboseOutput_)
							System.out.println("Zip file " + zipFile + " referenced in Install.bat, using that file instead");
					}
				}

				List<File> filesInZip = readFilesInZip(zipFile);
				if (!fixupFileLocations(filesInZip, profile, zipFile)) {
					String main = autoexec.getGameMain();
					if (StringUtils.isNotEmpty(main))
						System.out.println("WARNING: " + gameDirName + ": Main file [" + main + "] not found inside [" + zipFile + "]");
				}

				boolean multipleRootEntries = isMultipleRootEntries(filesInZip);
				if (multipleRootEntries) {
					autoexec.setBaseDir(gameDir);
					if (verboseOutput_)
						System.out.println("INFO: " + gameDirName + " is moved one directory level deeper");
				}

				autoexec.migrate(FileLocationService.getInstance().getDosrootLocation(), new FileLocation(gameDirName, FileLocationService.getInstance().dosrootRelative()));

				gamePack.getEntries().add(new GamePackEntry(i, profile, gameDirName, capFiles, extraFiles, extraDstFiles, zipFile));

			} catch (IOException e) {
				System.out.println("SKIPPED " + gameDirName + " " + e.toString());
			}
		}
		Collections.sort(gamePack.getEntries());

		System.out.println("Meagre meta-data analysis done");
		return gamePack;
	}

	private static GamePack analyzeLaunchBoxMetaData(File gamesGamesDir, File xodosTmpDir, GamePack gamePack) {
		System.out.println();
		System.out.println("==========================================");
		System.out.println(" Now analyzing LaunchBox meta-data");
		System.out.println("==========================================");
		System.out.println("Reading from: [" + xodosTmpDir + "]");

		Collection<File> allImagesList = FileUtils.listFiles(new File(xodosTmpDir, MAIN_METADATA_IMGDIR.getPath()), null, true);

		Map<String, List<File>> mapGameToFiles = new HashMap<>();

		for (File img: allImagesList) {
			Matcher imgmountMatcher = IMG_FILENAME_PTRN.matcher(img.getName());
			if (imgmountMatcher.matches() && (imgmountMatcher.groupCount() == 4)) {
				String title = imgmountMatcher.group(1);
				if (mapGameToFiles.containsKey(title)) {
					mapGameToFiles.get(title).add(img);
				} else {
					mapGameToFiles.put(title, new ArrayList<>(Arrays.asList(img)));
				}
			} else {
				System.out.println("Unusual image filename format [" + img.getPath() + "], skipped");
			}
		}

		try {
			File xmlFile = new File(xodosTmpDir, XODOS_METADATA_XML);
			Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(xmlFile);
			XPath xPath = XPathFactory.newInstance().newXPath();

			NodeList gameNodes = (NodeList)xPath.evaluate("/LaunchBox/Game", doc, XPathConstants.NODESET);

			for (int i = 0; i < gameNodes.getLength(); i++) {
				Element gameNode = (Element)gameNodes.item(i);

				String titleString = XmlUtils.getTextValue(gameNode, "Title");

				String rootFolderString = XmlUtils.getTextValue(gameNode, "RootFolder");
				File rootFolder = new File(rootFolderString);
				String gameDirName = rootFolder.getName();

				GamePackEntry existingGamePackEntry = gamePack.findEntryByGameDir(gameDirName);

				if (existingGamePackEntry == null) {
					System.out.println("Skipping LaunchBox metadata of [" + titleString + "] since [" + rootFolderString + "] was not found in Meagre metadata");
				} else {

					addLink(XmlUtils.getTextValue(gameNode, "ManualPath"), existingGamePackEntry, xodosTmpDir, gamesGamesDir, gameDirName);
					addLink(XmlUtils.getTextValue(gameNode, "MusicPath"), existingGamePackEntry, xodosTmpDir, gamesGamesDir, gameDirName);

					/*
					 * Notes Developer Platform Publisher Genre
					 */
					String titleToSearch = titleString.replace("/'", "_").replace("':", "_").replace("/", "_").replace("?", "_").replace(":", "_").replace("'", "_").replace("*", "_").replace("\\",
						"_").replace("\u2219", "\u00B7").replace("\u014D", "o").replace("\u0142", "l").replace("\u015B", "s");

					List<File> matchingImages = mapGameToFiles.get(titleToSearch);

					if (matchingImages == null) {
						if (verboseOutput_)
							System.out.println("Images for game [" + titleString + "] not found");
					} else {
						existingGamePackEntry.getCapturesList().addAll(matchingImages);

						existingGamePackEntry.getCapturesList().retainAll(existingGamePackEntry.getCapturesList().stream().collect(Collectors.toMap(x -> {
							try {
								return FileUtils.checksumCRC32(x);
							} catch (IOException e) {
								e.printStackTrace();
								return -1L;
							}
						}, p -> p, (p, q) -> p)).values());
					}
				}
			}

		} catch (SAXException | IOException | ParserConfigurationException | XPathExpressionException e) {
			e.printStackTrace();
		}

		System.out.println("LaunchBox meta-data analysis done");
		return gamePack;
	}

	private static void addLink(String title, String dst, LinkedHashMap<Long, File> extraFiles, List<File> extraDstFiles, List<String> linkTitlesList, List<String> linksList, boolean manualDir,
			File meagreDir, String gameDirName) {
		if (StringUtils.isNotBlank(dst)) {
			if (!dst.toLowerCase().startsWith("http")) {
				File extraFile = new File(meagreDir, (manualDir ? MANUAL_DIR: EXTRAS_DIR) + File.separatorChar + dst);
				if (FilesUtils.isExistingFile(extraFile)) {
					Long crc = existsInFileList(extraFile, extraFiles);
					if (crc != null) {
						final StringBuffer suggestion = new StringBuffer(dst);
						int i = 1;
						do {
							if (i > 1) {
								suggestion.setLength(0);
								suggestion.append(FilenameUtils.getBaseName(dst) + "(" + i + ")" + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(dst));
							}
							i++;
						} while (extraDstFiles.stream().anyMatch(x -> x.getName().equalsIgnoreCase(suggestion.toString())));

						extraFiles.put(crc, extraFile);
						extraDstFiles.add(new File(meagreDir, (manualDir ? MANUAL_DIR: EXTRAS_DIR) + File.separatorChar + suggestion));

						if (linkTitlesList.size() < Profile.NR_OF_LINK_TITLES) {
							linkTitlesList.add(StringUtils.isBlank(title) ? suggestion.toString(): title);
							linksList.add(FileLocationService.DOSROOT_DIR_STRING + gameDirName + File.separatorChar + EXTRAS_DIR + File.separatorChar + suggestion);
						}
					}
				} else if (verboseOutput_)
					System.out.println(gameDirName + ": linked file [" + extraFile + "] not found, skipped");
			} else {
				if (linkTitlesList.size() < Profile.NR_OF_LINK_TITLES) {
					linkTitlesList.add(title);
					linksList.add(dst);
				}
			}
		}
	}

	private static void addLink(String dst, GamePackEntry gamePackEntry, File xodosTmpDir, File gamesGamesDir, String gameDirName) {
		if (StringUtils.isNotBlank(dst)) {
			File extraFile = dst.startsWith(EXODOS_V5_GAMEZIPS_DIR) ? new File(new File(xodosTmpDir.getParentFile(), FilenameUtils.getBaseName(DOS_METADATA_ARC)), dst)
					: (dst.startsWith(EXODOS_DIR) ? new File(gamesGamesDir.getParentFile().getParentFile().getParentFile(), dst): new File(xodosTmpDir, dst));
			if (FilesUtils.isExistingFile(extraFile)) {
				Long crc = existsInFileList(extraFile, gamePackEntry.getExtrasList());
				if (crc != null) {
					final StringBuffer suggestion = new StringBuffer(extraFile.getName());
					int i = 1;
					do {
						if (i > 1) {
							suggestion.setLength(0);
							suggestion.append(FilenameUtils.getBaseName(extraFile.getName()) + "(" + i + ")" + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(extraFile.getName()));
						}
						i++;
					} while (gamePackEntry.getExtrasDstList().stream().anyMatch(x -> x.getName().equalsIgnoreCase(suggestion.toString())));

					gamePackEntry.getExtrasList().put(crc, extraFile);
					gamePackEntry.getExtrasDstList().add(
						dst.startsWith(EXODOS_DIR) ? new File(gamesGamesDir.getParentFile().getParentFile().getParentFile(), suggestion.toString()): new File(xodosTmpDir, suggestion.toString()));

					int idx = gamePackEntry.getIndexFirstEmptyLink();
					if (idx != -1) {
						gamePackEntry.getProfile().getLinks()[idx] = new Link(extraFile.getName(),
								FileLocationService.DOSROOT_DIR_STRING + gameDirName + File.separatorChar + EXTRAS_DIR + File.separatorChar + extraFile.getName());
					}
				}
			} else if (verboseOutput_)
				System.out.println(gameDirName + ": linked file [" + extraFile + "] not found, skipped");
		}
	}

	private static void extractV5MetaData(File inputDir, File xodosMetadataFile, File tmpDirForXodosZip, File dosMetadataFile, File tmpDirForDosZip) {
		System.out.println();
		System.out.println("==========================================");
		System.out.println(" Phase 1 of 3: Extracting meta-data");
		System.out.println("==========================================");
		System.out.println("Reading from: [" + inputDir + "]");

		extractLaunchBoxMetaData(false, xodosMetadataFile, tmpDirForXodosZip);
		extractLaunchBoxMetaData(false, dosMetadataFile, tmpDirForDosZip);
	}

	private static GamePack analyzeV5MetaData(DosboxVersion defaultDosboxVersion, File gameZipsDir, File tmpDirForXodosZip, File tmpDirForDosZip) {
		System.out.println();
		System.out.println("==========================================");
		System.out.println(" Phase 2 of 3: Analyzing meta-data");
		System.out.println("==========================================");
		System.out.println("Reading from: [" + gameZipsDir + "]");

		GamePack gamePack = new GamePack();
		gamePack.setCreationApp(CONVERTER_TITLE);
		gamePack.setCreationAppVersion(CONVERTER_VERSION);
		gamePack.setCreationDate(new Date());
		gamePack.setTitle(GPA_TITLE);
		gamePack.setAuthor(GPA_AUTHOR);
		gamePack.setNotes(GPA_NOTES);
		gamePack.setCapturesAvailable(true);
		gamePack.setGamedataAvailable(true);
		gamePack.setMapperfilesAvailable(false);
		gamePack.setNativecommandsAvailable(false);
		gamePack.setVersion(ImportExportProfilesService.PROFILES_XML_FORMAT_VERSION);
		gamePack.setDosboxVersions(Collections.singleton(defaultDosboxVersion));

		Collection<File> allImagesList = FileUtils.listFiles(new File(tmpDirForXodosZip, MAIN_METADATA_IMGDIR.getPath()), null, true);

		Map<String, List<File>> mapGameToFiles = new HashMap<>();

		for (File img: allImagesList) {
			Matcher imgmountMatcher = IMG_FILENAME_PTRN.matcher(img.getName());
			if (imgmountMatcher.matches() && (imgmountMatcher.groupCount() == 4)) {
				String title = imgmountMatcher.group(1).toLowerCase();
				if (mapGameToFiles.containsKey(title)) {
					mapGameToFiles.get(title).add(img);
				} else {
					mapGameToFiles.put(title, new ArrayList<>(Arrays.asList(img)));
				}
			} else {
				System.out.println("Unusual image filename format [" + img.getPath() + "], skipped");
			}
		}

		try {
			File xmlFile = new File(tmpDirForXodosZip, XODOS_METADATA_XML);
			Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(xmlFile);
			XPath xPath = XPathFactory.newInstance().newXPath();

			NodeList gameNodes = (NodeList)xPath.evaluate("/LaunchBox/Game", doc, XPathConstants.NODESET);

			for (int i = 0; i < gameNodes.getLength(); i++) {

				if ((i % 360) == 0) {
					String info = String.format("Analysis: %3.1f%%", (double)i / (double)gameNodes.getLength() * 100d);
					System.out.println(info);
				}

				String gameDirName = null;
				try {
					Element gameNode = (Element)gameNodes.item(i);

					String applicationPath = XmlUtils.getTextValue(gameNode, "ApplicationPath");
					if (StringUtils.isBlank(applicationPath))
						continue;

					File gameSrcZipFile = new File(gameZipsDir, FilenameUtils.getBaseName(applicationPath) + ".zip");
					if (!FilesUtils.isExistingFile(gameSrcZipFile)) {
						System.out.println("Zip file " + gameSrcZipFile + " is missing, skipping");
						continue;
					}

					String titleString = StringUtils.defaultString(XmlUtils.getTextValue(gameNode, "Title"));
					String developerString = StringUtils.defaultString(XmlUtils.getTextValue(gameNode, "Developer"));
					String publisherString = StringUtils.defaultString(XmlUtils.getTextValue(gameNode, "Publisher"));
					String genreString = StringUtils.defaultString(XmlUtils.getTextValue(gameNode, "Genre"));
					String yearString = StringUtils.left(StringUtils.defaultString(XmlUtils.getTextValue(gameNode, "ReleaseDate")), 4);
					String notesString = StringUtils.defaultString(XmlUtils.getTextValue(gameNode, "Notes"));
					String ratingString = XmlUtils.getTextValue(gameNode, "CommunityStarRating");

					String rootFolderString = XmlUtils.getTextValue(gameNode, "RootFolder");
					File rootFolder = new File(rootFolderString);
					gameDirName = rootFolder.getName();

					boolean favorite = false;
					String[] links = new String[Profile.NR_OF_LINK_DESTINATIONS];
					Arrays.fill(links, StringUtils.EMPTY);
					String[] linkTitles = new String[Profile.NR_OF_LINK_TITLES];
					Arrays.fill(linkTitles, StringUtils.EMPTY);

					List<File> capFiles = new ArrayList<>();
					LinkedHashMap<Long, File> extraFiles = new LinkedHashMap<>();
					List<File> extraDstFiles = new ArrayList<>();

					File gameDir = new File(tmpDirForDosZip, rootFolder.getPath());

					String confPathAndFile = new File(gameDir, FileLocationService.DOSBOX_CONF_STRING).getPath();

					List<String> linksList = new ArrayList<>();
					List<String> linkTitlesList = new ArrayList<>();
					File[] extras = new File(gameDir, EXTRAS_DIR).listFiles(new FileFilter() {
						@Override
						public boolean accept(File file) {
							return file.isFile() && Stream.of(EXTRAFILES).anyMatch(x -> x.equalsIgnoreCase(FilenameUtils.getExtension(file.getName())));
						}
					});
					for (File extraFile: extras)
						addLink(StringUtils.EMPTY, extraFile.getName(), extraFiles, extraDstFiles, linkTitlesList, linksList, false, gameDir, gameDirName);

					for (int j = 0; j < Math.min(links.length, linksList.size()); j++) {
						links[j] = linksList.get(j);
						linkTitles[j] = linkTitlesList.get(j);
					}

					Profile profile = ProfileFactory.create(i, titleString, developerString, publisherString, genreString, yearString, StringUtils.EMPTY, notesString, favorite, links, linkTitles,
						defaultDosboxVersion, confPathAndFile);

					if (StringUtils.isNotBlank(ratingString)) {
						profile.setCustomInt(0, String.valueOf((int)(Double.parseDouble(ratingString) * 20d)));
					}

					System.out.print(profile.resetAndLoadConfiguration());

					// minor bit of clean up of the actual dosbox configuration
					if ("64".equals(profile.getConfiguration().getValue("dosbox", "memsize")))
						profile.getConfiguration().setValue("dosbox", "memsize", "63");
					if ("overlay".equals(profile.getConfiguration().getValue("sdl", "output")))
						profile.getConfiguration().removeValue("sdl", "output");

					Autoexec autoexec = profile.getConfiguration().getAutoexec();
					autoexec.migrate(new FileLocation(EXODOS_DIR, FileLocationService.getInstance().dosrootRelative()), FileLocationService.getInstance().getDosrootLocation());

					List<File> filesInZip = readFilesInZip(gameSrcZipFile);
					if (!fixupFileLocations(filesInZip, profile, gameSrcZipFile)) {
						String main = autoexec.getGameMain();
						if (StringUtils.isNotEmpty(main))
							System.out.println("WARNING: " + gameDirName + ": Main file [" + main + "] not found inside [" + gameSrcZipFile + "]");
					}

					boolean multipleRootEntries = isMultipleRootEntries(filesInZip);
					if (multipleRootEntries) {
						autoexec.setBaseDir(gameDir);
						if (verboseOutput_)
							System.out.println("INFO: " + gameDirName + " is moved one directory level deeper");
					}

					autoexec.migrate(FileLocationService.getInstance().getDosrootLocation(), new FileLocation(gameDirName, FileLocationService.getInstance().dosrootRelative()));

					GamePackEntry newGamePackEntry = new GamePackEntry(i, profile, gameDirName, capFiles, extraFiles, extraDstFiles, gameSrcZipFile);

					String titleToSearch = titleString.toLowerCase().replace("/'", "_").replace("':", "_").replace("/", "_").replace("?", "_").replace(":", "_").replace("'", "_").replace("*",
						"_").replace("\\", "_").replace("\u2219", "\u00B7").replace("\u014D", "o").replace("\u0142", "l").replace("\u015B", "s").replace("\u010D", "c").replace("\u25CF",
							"#u25cf").replace("\u015B", "s").replace("\u0107", "c");

					List<File> matchingImages = mapGameToFiles.get(titleToSearch);

					if (matchingImages == null) {
						if (verboseOutput_)
							System.out.println("Images for game [" + titleString + "] not found");
					} else {
						newGamePackEntry.getCapturesList().addAll(matchingImages);

						newGamePackEntry.getCapturesList().retainAll(newGamePackEntry.getCapturesList().stream().collect(Collectors.toMap(x -> {
							try {
								return FileUtils.checksumCRC32(x);
							} catch (IOException e) {
								e.printStackTrace();
								return -1L;
							}
						}, p -> p, (p, q) -> p)).values());
					}

					addLink(XmlUtils.getTextValue(gameNode, "ManualPath"), newGamePackEntry, tmpDirForXodosZip, gameZipsDir, gameDirName);
					addLink(XmlUtils.getTextValue(gameNode, "MusicPath"), newGamePackEntry, tmpDirForXodosZip, gameZipsDir, gameDirName);

					gamePack.getEntries().add(newGamePackEntry);

				} catch (IOException e) {
					System.out.println("SKIPPED " + gameDirName + " " + e.toString());
				}
			}
		} catch (SAXException | ParserConfigurationException | XPathExpressionException | IOException e) {
			e.printStackTrace();
		}

		Collections.sort(gamePack.getEntries());
		System.out.println("Meta-data analysis done");
		return gamePack;
	}

	private static void generateGamePackArchives(GamePack gamePack, File tmpDir, String baseFilename) {
		System.out.println();
		System.out.println("===========================================");
		System.out.println(" Phase 3 of 3: Generating GamePackArchives");
		System.out.println("===========================================");

		List<List<GamePackEntry>> allGamePacksToCreate = new ArrayList<>();
		List<GamePackEntry> remainingGamePackEntries = new ArrayList<>(gamePack.getEntries());

		while (!remainingGamePackEntries.isEmpty()) {

			long totalSize = 0L;
			List<GamePackEntry> currentGamePackEntries = new ArrayList<>();
			boolean reachedMaxSize = false;

			while (!remainingGamePackEntries.isEmpty() && !reachedMaxSize) {
				GamePackEntry gamePackEntry = remainingGamePackEntries.get(0);
				try {
					long gameSize = determineSize(gamePackEntry);
					if (currentGamePackEntries.isEmpty() || ((totalSize + gameSize) < (maxPartSizeInMB_ * BYTES_IN_MB))) {
						currentGamePackEntries.add(gamePackEntry);
						remainingGamePackEntries.remove(0);
						totalSize += gameSize;
					} else {
						reachedMaxSize = true;
					}
				} catch (IOException e) {
					System.out.println("skipping " + gamePackEntry.getProfile().getTitle() + ", " + e.toString());
					remainingGamePackEntries.remove(0);
				}
			}

			allGamePacksToCreate.add(currentGamePackEntries);
		}

		ThreadPoolExecutor executor = (ThreadPoolExecutor)Executors.newFixedThreadPool(nrOfThreads_);

		for (List<GamePackEntry> gamePackEntries: allGamePacksToCreate) {
			executor.submit(() -> {
				File currentOutputGpa = new File(tmpDir,
						baseFilename + "__" + FilesUtils.toSafeFilename(gamePackEntries.get(0).getProfile().getTitle())
								+ (gamePackEntries.size() > 1 ? " - " + FilesUtils.toSafeFilename(gamePackEntries.get(gamePackEntries.size() - 1).getProfile().getTitle()): StringUtils.EMPTY)
								+ FilesUtils.GAMEPACKARCHIVE_EXT);

				try (ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(currentOutputGpa))) {
					for (GamePackEntry game: gamePackEntries) {
						try {
							Profile prof = game.getProfile();
							System.out.print("\n" + prof.getTitle() + " ");

							try {
								List<File> capsList = game.getCapturesList();
								for (int c = 0; c < capsList.size(); c++) {
									File srcCapFile = capsList.get(c);
									ZipUtils.zipEntry(zipOutputStream, srcCapFile, new File(game.getArchiveCapturesDir(), String.format("%02d", c) + "_" + srcCapFile.getName()), new IgnoreProgress());
								}
							} catch (IOException e) {
								throw new IOException(TEXT.get("dialog.export.error.exportcaptures", new Object[] {prof.getTitle(), StringRelatedUtils.toString(e)}), e);
							}

							File relativeGameDirInZip = game.getArchiveGameDir();
							File relativeExtrasGameDirInZip = new File(relativeGameDirInZip, EXTRAS_DIR);
							try {
								for (int i = 0; i < game.getExtrasList().size(); i++) {
									File srcExtraFile = (File)game.getExtrasList().values().toArray()[i];
									File dstExtraFile = game.getExtrasDstList().get(i);
									ZipUtils.zipEntry(zipOutputStream, srcExtraFile, new File(relativeExtrasGameDirInZip, dstExtraFile.getName()), new IgnoreProgress());
								}
								System.out.print('.');
								copyZipData(game.getZipFile(), relativeGameDirInZip, zipOutputStream);
							} catch (IOException e) {
								throw new IOException(TEXT.get("dialog.export.error.exportgamedata", new Object[] {prof.getTitle(), StringRelatedUtils.toString(e)}), e);
							}

						} catch (IOException e2) {
							System.out.println(
								"\nWARNING: The file [" + game.getZipFile() + "] could not be copied (completely) properly into the [" + currentOutputGpa + "], this game may be corrupt");
							e2.printStackTrace();
						}
					}

					ImportExportProfilesService.export(gamePack, gamePackEntries, zipOutputStream);
				} catch (ParserConfigurationException | TransformerException | IOException e) {
					e.printStackTrace();
				}

				System.out.println("\nDBGL GamePackArchive " + currentOutputGpa + " successfully generated");
				return null;
			});
		}

		executor.shutdown();

		try {
			executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		System.out.println("\n\nFinished.");
	}

	private static void cleanup(File gamesDir) {
		try {
			org.apache.commons.io.FileUtils.deleteDirectory(gamesDir);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private static Long existsInFileList(File extraFile, LinkedHashMap<Long, File> extrasList) {
		try {
			long crc = FileUtils.checksumCRC32(extraFile);
			return extrasList.containsKey(crc) ? null: crc;
		} catch (IOException e) {
			e.printStackTrace();
			return 0L;
		}
	}

	private static boolean isMultipleRootEntries(List<File> list) {
		int found = 0;
		for (File file: list) {
			if ((file.getParentFile() == null) && (++found > 1))
				return true;
		}
		return false;
	}

	private static boolean fixupFileLocations(List<File> list, Profile profile, File zipFile) {
		for (Mount m: profile.getNettoMountingPoints()) {
			if (m instanceof ImageMount) {
				File[] files = ((ImageMount)m).getImgPaths();
				String[] newFiles = ((ImageMount)m).getImgPathStrings();
				for (int i = 0; i < files.length; i++) {
					File f = files[i];
					if (list.stream().noneMatch(x -> x.getPath().equalsIgnoreCase(f.getPath()))) {
						File dst = list.stream().filter(x -> x.getName().equalsIgnoreCase(f.getName())).map(x -> new File(x.getParentFile(), x.getName())).findFirst().orElse(null);
						if (dst != null) {
							newFiles[i] = dst.getPath();
							if (verboseOutput_)
								System.out.println("Image-mounted file [" + f + "] redirected to [" + dst + "]");
						} else {
							System.out.println("WARNING: Image-mounted file [" + f + "] not found inside [" + zipFile + "]");
						}
					}
				}
				((ImageMount)m).setImgPaths(newFiles);
			}
		}

		Autoexec autoexec = profile.getConfiguration().getAutoexec();
		String main = autoexec.getGameMain();
		File mainFile = new File(main);

		int isoIdx = containsIso(mainFile.getPath());
		if (isoIdx != -1)
			mainFile = new File(mainFile.getPath().substring(0, isoIdx));

		int fatIdx = FilesUtils.fatImageIndex(mainFile.getPath());
		if (fatIdx != -1)
			mainFile = new File(mainFile.getPath().substring(0, fatIdx));

		if (findMainFile(list, autoexec, profile.getNettoMountingPoints(), mainFile))
			return true;

		if (mainFile.getName().contains("~")) {
			List<File> shortFilesList = ShortFilenameUtils.convertToShortFileSet(list);
			return findMainFile(shortFilesList, autoexec, profile.getNettoMountingPoints(), mainFile);
		}

		return false;
	}

	private static int containsIso(String mountPath) {
		for (String ext: CDIMAGES) {
			int idx = mountPath.toLowerCase().indexOf(ext + File.separatorChar);
			if (idx != -1)
				return idx + ext.length();
		}
		return -1;
	}

	private static boolean findMainFile(List<File> list, Autoexec autoexec, List<Mount> nettoMountingPoints, File mainFile) {
		if (list.stream().anyMatch(x -> x.getPath().equalsIgnoreCase(mainFile.getPath())))
			return true;

		File newMainFile = findSuitableExtension(mainFile, list);
		if (newMainFile != null) {
			autoexec.setGameMain(newMainFile.getPath());
			if (verboseOutput_)
				System.out.println("Main file [" + mainFile + "] was using wrong file extension, changed to [" + newMainFile.getPath() + "]");
			return true;
		}

		File parent1 = mainFile.getParentFile();
		if (parent1 != null) {
			File parent2 = parent1.getParentFile();
			if (parent2 != null) {
				newMainFile = new File(parent2, mainFile.getName());
				String newMainFilePath = newMainFile.getPath();
				if (list.stream().anyMatch(x -> x.getPath().equalsIgnoreCase(newMainFilePath))) {
					autoexec.setGameMainPath(parent2);
					if (verboseOutput_)
						System.out.println("Main file [" + mainFile + "] redirected to parent directory [" + parent2.getPath() + "]");
					return true;
				}

				newMainFile = findSuitableExtension(newMainFile, list);
				if (newMainFile != null) {
					autoexec.setGameMain(newMainFile.getPath());
					if (verboseOutput_)
						System.out.println("Main file [" + mainFile + "] was using wrong file extension and dir, changed to [" + newMainFile.getPath() + "]");
					return true;
				}
			}
		}

		String[] setPaths = autoexec.getSetPathsFromCustomSections();
		if (setPaths != null && mainFile.getName().toLowerCase().startsWith("win")) {
			File mainBaseFolder = mainFile.getParentFile();
			for (String setPath: setPaths) {
				char pd = setPath.toUpperCase().charAt(0);

				for (Mount m: nettoMountingPoints) {
					if (m instanceof DirMount && m.getDrive() == pd) {
						File cp = new File(((DirMount)m).getPathString(), setPath.substring(3));
						File f1 = new File(cp, mainFile.getName());
						newMainFile = findSuitableExtension(f1, list);
						if (newMainFile != null) {
							autoexec.setGameMain(newMainFile.getPath());
							if (verboseOutput_)
								System.out.println("Main file [" + mainFile + "] located using set path, changed to [" + newMainFile.getPath() + "]");

							// Check and fix path to Windows parameter executable(s)
							String params = autoexec.getParameters();
							if (StringUtils.isNotEmpty(params)) {
								String[] paramArray = StringUtils.split(params);
								String[] fixedParamArray = StringUtils.split(params);
								for (int i = 0; i < paramArray.length; i++) {
									if (paramArray[i].startsWith("/") || paramArray[i].startsWith("-"))
										continue; // unlikely to be file parameter, accept in any case

									String p = fixParameterPath(list, mainBaseFolder, ((DirMount)m).getPathString(), paramArray[i]);
									if (p == null) {
										if (verboseOutput_)
											System.out.println("INFO: Parameter [" + paramArray[i] + "] not found, might not be a file or folder");
									} else {
										fixedParamArray[i] = p;
									}
								}
								autoexec.setParameters(StringUtils.join(fixedParamArray, ' '));
								if (verboseOutput_)
									System.out.println("Main file parameter(s) [" + params + "] changed to [" + autoexec.getParameters() + "]");
							}
							return true;
						}
					}
				}
			}
		}

		// last resort, accept any location inside zip
		newMainFile = list.stream().filter(x -> x.getName().equalsIgnoreCase(mainFile.getName())).findFirst().orElse(null);
		if (newMainFile != null) {
			autoexec.setGameMain(newMainFile.getPath());
			if (verboseOutput_)
				System.out.println("Main file [" + mainFile + "] located elsewhere in zip, changed to [" + newMainFile.getPath() + "]");
			return true;
		}

		return false;
	}

	private static String fixParameterPath(List<File> list, File mainBaseFolder, String mountPath, String param) {
		File newParamFile = findSuitableExtension(new File(FilenameUtils.normalize(new File(mainBaseFolder, param).getPath())), list);
		if (newParamFile != null)
			return newParamFile.getPath().substring(mountPath.length());
		newParamFile = findSuitableExtension(new File(FilenameUtils.normalize(new File(mainBaseFolder.getParentFile(), param).getPath())), list);
		if (newParamFile != null)
			return newParamFile.getPath().substring(mountPath.length());
		newParamFile = findSuitableExtension(new File(FilenameUtils.normalize(new File(mountPath, param).getPath())), list);
		if (newParamFile != null)
			return newParamFile.getPath().substring(mountPath.length());
		return null;
	}

	private static String findMainFolder(File zipFile) throws IOException {
		try (ZipFile zfile = new ZipFile(zipFile, CP437)) {
			for (Enumeration<? extends ZipEntry> entries = zfile.entries(); entries.hasMoreElements();) {
				try {
					ZipEntry entry = entries.nextElement();
					if (entry.isDirectory())
						return getZipEntryName(entry);
				} catch (@SuppressWarnings("unused") IllegalArgumentException e) {
					System.out.println("WARNING: Zip file [" + zipFile + "] contains an entry with problematic characters in its filename");
				}
			}
		}
		return null;
	}

	private static String getZipEntryName(ZipEntry entry) {
		return entry.getName().replace((char)15, '\u263C');
	}

	private static List<File> readFilesInZip(File zipFile) throws IOException {
		List<File> result = new ArrayList<>();
		try (ZipFile zfile = new ZipFile(zipFile, CP437)) {
			for (Enumeration<? extends ZipEntry> entries = zfile.entries(); entries.hasMoreElements();) {
				try {
					ZipEntry entry = entries.nextElement();
					if (!entry.isDirectory())
						result.add(new File(FilesUtils.toNativePath(getZipEntryName(entry))));
				} catch (@SuppressWarnings("unused") IllegalArgumentException e) {
					System.out.println("WARNING: Zip file [" + zipFile + "] contains an entry with problematic characters in its filename");
				}
			}
		}
		return result;
	}

	private static List<File> readFoldersInZip(File zipFile) throws IOException {
		List<File> result = new ArrayList<>();
		try (ZipFile zfile = new ZipFile(zipFile, CP437)) {
			for (Enumeration<? extends ZipEntry> entries = zfile.entries(); entries.hasMoreElements();) {
				try {
					ZipEntry entry = entries.nextElement();
					File filename = new File(FilesUtils.toNativePath(getZipEntryName(entry)));
					if (entry.isDirectory() && !result.contains(filename)) {
						result.add(filename);
					} else {
						File folder = filename.getParentFile();
						if (folder != null && !result.contains(folder))
							result.add(folder);
					}
				} catch (@SuppressWarnings("unused") IllegalArgumentException e) {
					// Ignore, warning already given
				}
			}
		}
		return result;
	}

	private static long determineSize(GamePackEntry game) throws IOException {
		try {
			long result = FileUtils.sizeOf(game.getZipFile());
			result += game.getExtrasList().values().stream().mapToLong(x -> FileUtils.sizeOf(x)).sum();
			result += game.getCapturesList().stream().mapToLong(x -> FileUtils.sizeOf(x)).sum();
			result += 1024 * 4; // reserved 4 KB for profiles.xml data
			return result;
		} catch (Exception e) {
			throw new IOException("Could not determine game size " + e.toString());
		}
	}

	private static long sizeInBytes(ZipFile zf, String filename) {
		long bytes = 0;
		for (Enumeration<? extends ZipEntry> entries = zf.entries(); entries.hasMoreElements();) {
			try {
				bytes += entries.nextElement().getSize();
			} catch (@SuppressWarnings("unused") IllegalArgumentException e) {
				System.out.println("WARNING: Zip file [" + filename + "] contains an entry with problematic characters in its filename");
			}
		}
		return bytes;
	}

	private static void unzip(File zipFile, File dstDir) throws IOException {
		ProgressNotifyable prog = new ProgressNotifyable() {
			private long total_;
			private long progress_;
			private String lastInfo_;

			@Override
			public void setTotal(long total) {
				total_ = total;
			}

			@Override
			public void incrProgress(long progress) {
				progress_ += progress;
				String info = String.format("\rExtracting %s: %3.1f%%", zipFile, progress_ * 100.0 / total_);
				if (!info.equals(lastInfo_)) {
					System.out.print(info);
					lastInfo_ = info;
				}
			}

			@Override
			public void setProgress(long progress) {
			}
		};

		try (ZipFile zfile = new ZipFile(zipFile, CP437)) {
			prog.setTotal(sizeInBytes(zfile, zipFile.getPath()));
			for (Enumeration<? extends ZipEntry> entries = zfile.entries(); entries.hasMoreElements();) {
				try {
					ZipEntry entry = entries.nextElement();
					File dstFile = new File(dstDir, getZipEntryName(entry));
					ZipUtils.extractEntry(zfile, entry, dstFile, prog);
				} catch (@SuppressWarnings("unused") IllegalArgumentException e) {
					System.out.println("WARNING: Zip file [" + zipFile + "] contains an entry with problematic characters in its filename");
				}
			}
			System.out.printf("\rExtracting %s: Done  \n", zipFile);
		}
	}

	@SuppressWarnings("resource")
	private static void copyZipData(File srcFile, File baseDirectory, ZipOutputStream zos) throws IOException {
		try (ZipFile srcZipFile = new ZipFile(srcFile, CP437)) {
			int i = 0;
			for (Enumeration<? extends ZipEntry> entries = srcZipFile.entries(); entries.hasMoreElements();) {
				try {
					ZipEntry srcEntry = entries.nextElement();
					ZipEntry dstEntry = new ZipEntry(FilesUtils.toArchivePath(new File(baseDirectory, getZipEntryName(srcEntry)), srcEntry.isDirectory()));
					dstEntry.setComment(srcEntry.getComment());
					dstEntry.setTime(srcEntry.getTime());
					zos.putNextEntry(dstEntry);
					if (!srcEntry.isDirectory())
						IOUtils.copy(srcZipFile.getInputStream(srcEntry), zos);
					zos.closeEntry();

					if (i++ % 100 == 0)
						System.out.print('.');
				} catch (@SuppressWarnings("unused") IllegalArgumentException e) {
					System.out.println("\nWARNING: Zip file [" + srcFile + "] contains an entry with problematic characters in its filename");
				}
			}
		}
	}

	private static File findSuitableExtension(File mainFile, List<File> list) {
		for (String extension: FilesUtils.EXECUTABLES) {
			File newMainFile = new File(FilenameUtils.removeExtension(mainFile.getPath()) + extension);
			if (list.stream().anyMatch(x -> x.getPath().equalsIgnoreCase(newMainFile.getPath())))
				return newMainFile;
		}
		return null; // not found
	}

}
