/*
 *  Copyright (C) 2006-2016  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 org.dbgl.util;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import javax.swing.filechooser.FileSystemView;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.dbgl.gui.GeneralPurposeDialogs;
import org.dbgl.gui.StreamGobbler;
import org.dbgl.loopy.iso9660.ISO9660FileEntry;
import org.dbgl.loopy.iso9660.ISO9660FileSystem;
import org.dbgl.model.DosboxVersion;
import org.dbgl.model.NativeCommand;
import org.dbgl.model.Profile;
import org.dbgl.model.ShortFile;
import org.dbgl.model.Template;
import org.dbgl.model.conf.Conf;
import org.dbgl.model.conf.SectionsWrapper;
import org.dbgl.model.conf.Settings;
import org.dbgl.waldheinz.fs.fat.BlockDevice;
import org.dbgl.waldheinz.fs.fat.FatFileSystem;
import org.dbgl.waldheinz.fs.fat.FatLfnDirectory;
import org.dbgl.waldheinz.fs.fat.FatLfnDirectoryEntry;
import org.dbgl.waldheinz.fs.util.FileDisk;
import org.eclipse.swt.widgets.Display;
import SevenZip.MyRandomAccessFile;
import SevenZip.Archive.IInArchive;
import SevenZip.Archive.SevenZipEntry;
import SevenZip.Archive.SevenZip.Handler;


public final class FileUtils {

	public static final class FilenameComparator implements Comparator<String> {
		public int compare(final String string1, final String string2) {
			int count1 = StringUtils.countMatches(string1, "\\");
			int count2 = StringUtils.countMatches(string2, "\\");
			if (count1 == count2) {
				return string1.compareTo(string2);
			} else {
				return count1 - count2;
			}
		}
	}

	public static final class FileComparator implements Comparator<File> {
		public int compare(final File file1, final File file2) {
			return new FilenameComparator().compare(file1.getPath(), file2.getPath());
		}
	}

	private static final int ZIP_BUFFER = 10240;

	private static final File DATA_DIR_FILE;
	private static final File DOSROOT_DIR_FILE;
	private static final File DOSBOX_DIR_FILE;
	private static final File TMPINST_DIR_FILE;
	private static final String TEMPLATES_DIR = "templates" + File.separatorChar;
	public static final String SETUP_CONF = "setup.conf";
	private static final String TEMPLATE_CONF = "template.conf";

	public static final String PROFILES_XML = "profiles.xml";
	public static final String DOSROOT_DIR = "dosroot" + File.separatorChar;
	public static final String CAPTURES_DIR = "captures" + File.separatorChar;
	public static final String MAPPER_DIR = "mapper" + File.separatorChar;
	public static final String PROFILES_DIR = "profiles" + File.separatorChar;
	public static final String EXPORT_DIR = "export" + File.separatorChar;
	public static final String XSL_DIR = "xsl" + File.separatorChar;
	public static final String XSL_EXT = ".xsl";
	public static final String XML_EXT = ".xml";
	public static final String CONF_EXT = ".conf";
	public static final String MAPPER_EXT = ".map";
	public static final String GAMEPACKARCHIVE_EXT = ".dbgl.zip";
	public static final String[] CDIMAGES = {".iso", ".cue", ".bin"};
	public static final String[] EXECUTABLES = {".exe", ".com", ".bat"};
	public static final String[] EXECUTABLES_UPPERCASE = {"EXE", "COM", "BAT"};
	public static final String[] ARCHIVES = {".zip", ".7z"};
	static final String[] FATIMAGES = {".ima"};
	static final String[] BOOTERIMAGES = {".cp2", ".dcf", ".img", ".jrc", ".td0"};
	static final String[] PICTURES = {".png", ".gif", ".jpg", ".tif", ".tiff", ".ico", ".bmp"};
	static final String[] SETUPFILES = {"setup.exe", "install.exe", "setsound.exe", "setup.bat", "config.exe", "setsound.bat", "sound.bat", "sound.exe", "install.com", "install.bat", "sndsetup.exe",
			"soundset.exe", "config.bat", "setup.com", "setsnd.exe", "setd.exe", "configur.exe", "uwsound.exe"};
	static final String[] UNLIKELYMAINFILES = {"dos4gw.exe", "readme.bat", "intro.exe", "loadpats.exe", "uvconfig.exe", "soundrv.com", "sblaster.com", "sound.bat", "univbe.exe", "midpak.com",
			"ultramid.exe", "mpscopy.exe", "readme.exe", "sbpro.com", "help.bat", "exists.com", "helpme.exe", "paudio.com", "bootdisk.exe", "pas16.com", "mssw95.exe", "setd.exe", "adlib.com",
			"sbclone.com", "sb16.com", "godir.com", "ibmsnd.com", "crack.com", "gf166.com", "__insth.bat", "adlibg.com", "space.com", "instgame.bat", "ibmbak.com", "pkunzjr.com", "nosound.com",
			"sview.exe", "mgraphic.exe", "title.exe", "misc.exe", "checkcd.bat", "patch.exe", "tansltl.com", "readme.com", "uninstal.exe", "source.com", "cmidpak.com", "vector.com", "view.exe",
			"rtm.exe", "eregcard.exe", "sndsys.com", "info.exe", "docshell.exe", "catalog.exe", "ipxsetup.exe", "yes.com", "stfx.com", "getkey.com", "lsize.com", "makepath.com", "sersetup.exe",
			"commit.exe", "_setup.exe", "end.exe", "what.exe", "setm.exe", "cvxsnd.com", "aria.com", "tgraphic.exe", "egraphic.exe", "smidpak.com", "tlivesa.com", "vmsnd.com", "detect.exe",
			"digvesa.com", "cwsdpmi.exe", "vesa.exe", "havevesa.exe", "_install.bat", "smsnd.com", "insticon.exe", "installh.bat", "install2.bat", "info.bat", "setmain.exe", "swcbbs.exe",
			"vbetest.exe", "pmidpak.com", "inst.exe", "cleardrv.exe", "winstall.exe", "ibm1bit.com", "tmidpak.com", "dealers.exe", "digisp.com", "drv_bz.com", "drv_sb.com", "drv_ss.com",
			"convert.exe", "editor.exe", "cgraphic.exe", "update.bat", "smackply.exe", "univesa.exe", "lha.exe", "makeboot.bat", "nnansi.com", "setblast.exe", "autoexec.bat", "helpme.bat",
			"exist.com", "fixboot.exe", "ask.com", "vesatest.exe", "manual.exe", "sbwave.com", "rmidpak.com", "diagnost.exe", "pkunzip.exe", "sinstall.exe", "megaem.exe", "vesa.com", "getdrv.exe",
			"drv_sbd.com", "chkvesa.exe", "chkmem.com", "setstick.exe"};

	public static final String DOSBOX_CONF = "dosbox.conf";
	public static final String CNF_FILTER = "*.conf;*.CONF";
	public static final String EXE_FILTER = "*.com;*.COM;*.exe;*.EXE;*.bat;*.BAT";
	public static final String ARC_FILTER = "*.zip;*.ZIP;*.7z;*.7Z";
	public static final String DBGLZIP_FILTER = "*.dbgl.zip;*.DBGL.ZIP";
	public static final String BTR_FILTER = "*.cp2;*.CP2;*.dcf;*.DCF;*.img;*.IMG;*.jrc;*.JRC;*.td0;*.TD0";
	public static final String CDI_FILTER = "*.iso;*.ISO;*.cue;*.CUE";
	public static final String FATI_FILTER = "*.ima;*.IMA;";
	public static final String ALL_FILTER = "*";

	public static final String INVALID_FILENAME_CHARS_REGEXP = "[^a-zA-Z_0-9()]";

	static {
		final SectionsWrapper settings = Settings.getInstance().getSettings();
		final String data = replaceTildeInPath(settings.getValue("directory", "data"));
		boolean dataInUserDir = PlatformUtils.USE_USER_HOME_DIR || !PlatformUtils.isDirectoryWritable(new File(data));
		DATA_DIR_FILE = canonical(dataInUserDir ? new File(PlatformUtils.USER_DATA_DIR_FILE, data).getPath(): data);
		DOSROOT_DIR_FILE = new File(DATA_DIR_FILE, DOSROOT_DIR);
		DOSBOX_DIR_FILE = canonical(replaceTildeInPath(settings.getValue("directory", "dosbox")));
		TMPINST_DIR_FILE = new File(DOSROOT_DIR_FILE, settings.getValue("directory", "tmpinstall"));

		if (dataInUserDir)
			copyDirectoriesIfNecessary(data);
		createDirIfNecessary(DOSROOT_DIR_FILE);
	}

	public static String replaceTildeInPath(final String path) {
		if ((PlatformUtils.IS_LINUX || PlatformUtils.IS_OSX) && (path.startsWith("~/") || (path.length() == 1 && path.charAt(0) == '~'))) {
			// Linux and OSX have ~/ for homedirectory. Allow single ~ as well
			return path.replaceAll("^~", System.getProperty("user.home"));
		}
		return path;
	}

	private static void copyDirectoriesIfNecessary(final String data) {
		copyDirIfNecessary(data, CAPTURES_DIR);
		File databaseFile = getDatabaseFile(Settings.getInstance().getSettings().getValue("database", "connectionstring"));
		if (databaseFile != null)
			copyDirIfNecessary(data, databaseFile.getParent());
		copyDirIfNecessary(data, DOSROOT_DIR);
		copyDirIfNecessary(data, EXPORT_DIR);
		copyDirIfNecessary(data, PROFILES_DIR);
		copyDirIfNecessary(data, TEMPLATES_DIR);
		copyDirIfNecessary(data, XSL_DIR);
	}

	private static void copyDirIfNecessary(final String data, final String dir) {
		File src = new File(data, dir);
		try {
			if (!isExistingDirectory(canonicalToData(dir)) && isExistingDirectory(src)) {
				org.apache.commons.io.FileUtils.copyDirectoryToDirectory(src, DATA_DIR_FILE);
			}
		} catch (IOException e) {
			System.err.println(Settings.getInstance().msg("general.error.copydirtodir", new String[] {src.getPath(), DATA_DIR_FILE.getPath()}));
		}
	}

	public static File getDatabaseFile(final String connString) {
		if (connString.contains("file:")) {
			// Some magic on the connection string
			int start = connString.indexOf("file:") + 5; // skip 'file:'
			int end = connString.indexOf(';', start);
			if (end == -1) {
				end = connString.length();
			}
			String filename = replaceTildeInPath(connString.substring(start, end));
			return canonicalToData(filename);
		} else {
			return null;
		}
	}

	public static boolean isExistingFile(final File file) {
		return file.isFile() && file.exists();
	}

	public static boolean isExistingDirectory(final File dir) {
		return dir.isDirectory() && dir.exists();
	}

	private static List<String> initCommands(final DosboxVersion dbversion, boolean forceDBConf) {
		List<String> execCommands = new ArrayList<String>();
		if (dbversion.isUsingCurses()) {
			if (PlatformUtils.IS_WINDOWS) {
				execCommands.add("rundll32");
				execCommands.add("SHELL32.DLL,ShellExec_RunDLL");
			} else if (PlatformUtils.IS_LINUX) {
				execCommands.add("xterm");
				execCommands.add("-e");
			}
		}
		execCommands.add(dbversion.getCanonicalExecutable().getPath());
		if ((dbversion.isMultiConfig() && isReadableFile(dbversion.getCanonicalConfFile())) || forceDBConf) {
			// selected default dosbox config file
			execCommands.add("-conf");
			execCommands.add(dbversion.getCanonicalConfFile().getPath());
		}
		return execCommands;
	}

	private static void postCommands(final DosboxVersion dbversion, List<String> execCommands) {
		if (dbversion.getParameters().length() > 0) {
			for (String p: dbversion.getParameters().split(" ")) {
				execCommands.add(p);
			}
		}
		if (Settings.getInstance().getSettings().getBooleanValue("dosbox", "hideconsole")) {
			execCommands.add("-noconsole");
		}
	}

	private static void executeCommand(final DosboxVersion dbversion, final List<String> execCommands, final File cwd, Map<String, String> env, final boolean waitFor) throws IOException {
		StringBuffer cmd = new StringBuffer();
		try {
			File dir = (cwd == null) ? DOSROOT_DIR_FILE: cwd;
			if (PlatformUtils.IS_OSX && dbversion != null && dbversion.isUsingCurses()) {
				String terminalCommand = StringUtils.join(execCommands, ' ');
				execCommands.clear();
				execCommands.add("osascript");
				execCommands.add("-e");
				execCommands.add("tell application \"Terminal\" to do script \"cd '" + dir + "'; " + terminalCommand + "; exit;\"");
			}
			System.out.print(StringUtils.join(execCommands, ' '));
			ProcessBuilder pb = new ProcessBuilder(execCommands.toArray(new String[execCommands.size()]));
			pb.directory(dir);
			Map<String, String> environment = pb.environment();
			if (env != null) {
				environment.putAll(env);
				System.out.print(env);
			}
			System.out.println();
			Process proc = pb.start();
			StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), "DOSBox stderr");
			StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), "DOSBox stdout");
			outputGobbler.start();
			errorGobbler.start();
			if (waitFor) {
				try {
					proc.waitFor();
				} catch (InterruptedException e) {}
			}
		} catch (IOException e) {
			e.printStackTrace();
			throw new IOException(Settings.getInstance().msg("general.error.startdosbox", new Object[] {cmd}));
		}
	}

	private static void doRunDosbox(final DosboxVersion dbversion, final String[] parameters, final boolean forceDBConf, final File cwd, final Map<String, String> env, final boolean waitFor)
			throws IOException {
		List<String> commandItems = initCommands(dbversion, forceDBConf);
		commandItems.addAll(Arrays.asList(parameters));
		postCommands(dbversion, commandItems);
		executeCommand(dbversion, commandItems, cwd, env, waitFor);
	}

	private static File canonicalTo(final File base, final String path) {
		File file = new File(path);
		if (file.isAbsolute()) {
			try {
				return file.getCanonicalFile();
			} catch (IOException e) {
				return file.getAbsoluteFile();
			}
		} else {
			try {
				return new File(base, file.getPath()).getCanonicalFile();
			} catch (IOException e) {
				return new File(base, file.getPath()).getAbsoluteFile();
			}
		}
	}

	public static boolean areRelated(final File parent, final File child) {
		File remainder = child.getParentFile();
		while (remainder != null) {
			if (parent.equals(remainder)) {
				return true;
			}
			remainder = remainder.getParentFile();
		}
		return false;
	}

	public static String getDosRoot() {
		return DOSROOT_DIR_FILE.getPath();
	}

	public static File makeRelativeTo(final File file, final File basePath) {
		if (!file.isAbsolute()) {
			return file;
		}
		if (file.equals(basePath)) {
			return new File(".");
		}
		File remainder = new File(file.getName());
		File parent = file.getParentFile();
		while (parent != null) {
			if (parent.equals(basePath)) {
				return remainder;
			}
			remainder = new File(parent.getName(), remainder.getPath());
			parent = parent.getParentFile();
		}
		return file;
	}

	public static File makeRelativeToData(final File file) {
		return makeRelativeTo(file, DATA_DIR_FILE);
	}

	public static File makeRelativeToDosroot(final File file) {
		return makeRelativeTo(file, DOSROOT_DIR_FILE);
	}

	public static File makeRelativeToDosbox(final File file) {
		return makeRelativeTo(file, DOSBOX_DIR_FILE);
	}

	public static File canonical(final String path) {
		return canonicalTo(new File("."), path);
	}

	public static File canonicalToData(final String path) {
		return canonicalTo(DATA_DIR_FILE, path);
	}

	public static File canonicalToDosbox(final String path) {
		return canonicalTo(DOSBOX_DIR_FILE, path);
	}

	public static File canonicalToDosroot(final String path) {
		return canonicalTo(DOSROOT_DIR_FILE, path);
	}

	public static String sanitizeToDosroot(final String path) {
		return makeRelativeToDosroot(canonicalToDosroot(path)).getPath();
	}

	public static File prefixAndSanitizeToDosroot(final File basePath, final File file) {
		if (!file.isAbsolute())
			return makeRelativeToDosroot(canonicalToDosroot(new File(basePath, file.getPath()).getPath()));
		return file;
	}

	public static File constructRelativeDBConfLocation(final String path) {
		return makeRelativeToDosbox(new File(path, DOSBOX_CONF));
	}

	public static File constructCanonicalDBExeLocation(final String path) {
		return canonicalToDosbox(new File(path, PlatformUtils.DB_EXECUTABLE).getPath());
	}

	public static String constructCapturesDir(final int profileId) {
		return CAPTURES_DIR + profileId;
	}

	public static String constructMapperFile(final int profileId) {
		return MAPPER_DIR + profileId + MAPPER_EXT;
	}

	public static String getRelativePath(final File base, final File name) throws IOException {
		File parent = base.getParentFile();
		if (parent == null)
			return name.getPath();

		String bpath = base.getCanonicalPath();
		String fpath = name.getCanonicalPath();

		if (fpath.startsWith(bpath)) {
			return fpath.substring(bpath.length() + 1);
		} else {
			return ".." + File.separatorChar + getRelativePath(parent, name);
		}
	}

	public static String constructRelativeCapturesDir(final int profileId, final File confFileDirectory, final int dosboxVersionGeneration) {
		File baseDir = null;
		if (dosboxVersionGeneration < 3) // older than 0.73
			baseDir = DOSROOT_DIR_FILE; // cwd when starting DOSBox
		else
			baseDir = confFileDirectory;// directory containing the profile's .conf

		File canCaps = canonicalToData(constructCapturesDir(profileId));
		try {
			return getRelativePath(baseDir, canCaps);
		} catch (IOException e) {
			return canCaps.getPath();
		}
	}

	public static File constructCanonicalTemplateFileLocation(final int templateId) {
		return canonicalToData(TEMPLATES_DIR + templateId + CONF_EXT);
	}

	public static File getDefaultTemplatesXmlFile() {
		return canonicalToData(TEMPLATES_DIR + "default.xml");
	}

	public static String constructUniqueConfigFileString(final int profileId, final String profileTitle, final File canonicalMainDir) {
		SectionsWrapper set = Settings.getInstance().getSettings();
		File path;
		if ((set.getIntValue("profiledefaults", "confpath") == 0) || (canonicalMainDir == null))
			path = new File(PROFILES_DIR);
		else
			path = canonicalMainDir;
		String prefix = (set.getIntValue("profiledefaults", "conffile") == 0) ? String.valueOf(profileId): profileTitle;
		File candidate = null;
		int nr = 1;
		do {
			candidate = new File(path, fileSystemSafe(prefix + ((nr > 1) ? "(" + nr + ")": "")) + CONF_EXT);
			nr++;
		} while (isExistingFile(canonicalToData(candidate.getPath())));
		return candidate.getPath();
	}

	public static void doRunDosbox(final DosboxVersion dbversion, final Map<String, String> env) throws IOException {
		doRunDosbox(dbversion, new String[] {}, true, null, env, false);
	}

	public static void doCreateDosboxConf(final DosboxVersion dbversion) throws IOException {
		char q = PlatformUtils.IS_WINDOWS ? '\'': '"';
		doRunDosbox(dbversion, new String[] {"-c", "config -writeconf " + q + dbversion.getCanonicalConfFile() + q, "-c", "exit"}, false, dbversion.getCanonicalExecutable().getParentFile(), null,
			true);
	}

	public static void doRunProfile(final Profile prof, final List<DosboxVersion> dbversions, final Map<String, String> env, final int setup, final boolean prepareOnly, final List<NativeCommand> cmds,
			final Display display) throws IOException {
		DosboxVersion dbv = DosboxVersion.findById(dbversions, prof.getDbversionId());
		doRunProfile(prof, dbv, env, setup, prepareOnly, cmds, display);
	}

	public static void doRunTemplate(final Template template, final List<DosboxVersion> dbversions, final Map<String, String> env, final List<NativeCommand> cmds, final Display display)
			throws IOException {
		DosboxVersion dbv = DosboxVersion.findById(dbversions, template.getDbversionId());
		doRunTemplate(template, dbv, env, cmds, display);
	}

	public static void doRunProfile(final Profile prof, final DosboxVersion dbversion, final Map<String, String> env, final int setup, final boolean prepareOnly, final List<NativeCommand> cmds,
			final Display display) throws IOException {
		final File file;
		boolean runSetup = (setup != -1) && prof.hasSetup(setup);
		if (runSetup || prepareOnly) {
			file = canonicalToData(PROFILES_DIR + SETUP_CONF);
			Conf conf = prepareOnly ? new Conf(prof, null, null, file, dbversion, System.err): new Conf(prof, prof.getSetup(setup), prof.getSetupParameters(setup), file, dbversion, System.err);
			conf.save(prepareOnly);
		} else {
			file = prof.getCanonicalConfFile();
		}

		new Thread() {
			public void run() {
				try {
					for (NativeCommand cmd: cmds) {
						if (cmd.getCommand() == null) {
							doRunDosbox(dbversion, new String[] {"-conf", file.getPath()}, false, null, env, true);
						} else {
							executeCommand(null, cmd.getExecCommandsCanToData(), cmd.getCwdCanToData(), env, cmd.isWaitFor());
						}
					}
				} catch (final IOException e) {
					if (!display.isDisposed()) {
						display.syncExec(new Runnable() {
							public void run() {
								GeneralPurposeDialogs.warningMessage(display.getActiveShell(), e);
							}
						});
					}
				}
			}
		}.start();
	}

	public static void doRunInstaller(final Profile prof, final Conf conf, final List<DosboxVersion> dbversions, final Map<String, String> env, final boolean prepareOnly) throws IOException {
		DosboxVersion dbv = DosboxVersion.findById(dbversions, prof.getDbversionId());
		File file = canonicalToData(PROFILES_DIR + SETUP_CONF);
		Conf tmpConf = new Conf(conf, file);
		tmpConf.save(prepareOnly);
		doRunDosbox(dbv, new String[] {"-conf", file.getPath()}, false, null, env, true);
	}

	public static void doRunTemplate(final Template template, final DosboxVersion dbversion, final Map<String, String> env, final List<NativeCommand> cmds, final Display display) throws IOException {
		final File file = canonicalToData(PROFILES_DIR + TEMPLATE_CONF);
		Conf conf = new Conf(template, file, dbversion, System.err);
		conf.save();

		new Thread() {
			public void run() {
				try {
					for (NativeCommand cmd: cmds) {
						if (cmd.getCommand() == null) {
							doRunDosbox(dbversion, new String[] {"-conf", file.getPath()}, false, null, env, true);
						} else {
							executeCommand(null, cmd.getExecCommandsCanToData(), cmd.getCwdCanToData(), env, cmd.isWaitFor());
						}
					}
				} catch (final IOException e) {
					if (!display.isDisposed()) {
						display.syncExec(new Runnable() {
							public void run() {
								GeneralPurposeDialogs.warningMessage(display.getActiveShell(), e);
							}
						});
					}
				}
			}
		}.start();
	}

	public static void createDir(final File dir) {
		if (!dir.exists() && !dir.mkdirs()) {
			System.err.println(Settings.getInstance().msg("general.error.createdir", new Object[] {dir}));
		}
	}

	public static void copyFiles(final File srcDir, final File dstDir) {
		File[] srcFiles = srcDir.listFiles();
		if (srcFiles != null) {
			for (File src: srcFiles) {
				if (src.isFile()) {
					File dst = new File(dstDir, src.getName());
					try {
						org.apache.commons.io.FileUtils.copyFile(src, dst, true);
					} catch (IOException e) {
						System.err.println(Settings.getInstance().msg("general.error.copyfile", new Object[] {src, dst}));
					}
				}
			}
		}
	}

	public static void fileSetLastModified(final File file, final long time) {
		if (!file.setLastModified(time)) {
			System.err.println(Settings.getInstance().msg("general.error.setlastmodifiedfile", new Object[] {file.getPath()}));
		}
	}

	public static void fileSetReadOnly(final File file) {
		if (!file.setReadOnly())
			System.err.println(Settings.getInstance().msg("general.error.setreadonlyfile", new Object[] {file.getPath()}));
	}

	public static void removeFile(final File file) {
		if (!(file.isFile() && file.delete())) {
			System.err.println(Settings.getInstance().msg("general.error.deletefile", new Object[] {file.getPath()}));
		}
	}

	public static void removeFilesInDirAndDir(final File dir) {
		File[] files = dir.listFiles();
		if (files != null) {
			for (File file: files) {
				if (file.isDirectory()) {
					System.err.println(Settings.getInstance().msg("general.error.dirtobedeletedcontainsdir", new Object[] {dir.getPath()}));
					return;
				}
			}
			for (File file: files) {
				if (!file.delete()) {
					System.err.println(Settings.getInstance().msg("general.error.deletefile", new Object[] {file}));
				}
			}
		}
		if (!(dir.isDirectory() && dir.delete())) {
			System.err.println(Settings.getInstance().msg("general.error.deletedir", new Object[] {dir.getPath()}));
		}
	}

	public static boolean isReadableFile(final File file) {
		return file.isFile() && file.canRead();
	}

	public static void createDirIfNecessary(File dir) {
		if (!dir.isDirectory()) {
			System.out.println(Settings.getInstance().msg("general.notice.createdir", new Object[] {dir.getPath()}));
			if (!dir.mkdirs()) {
				System.err.println(Settings.getInstance().msg("general.error.createdir", new Object[] {dir.getPath()}));
			}
		}
	}

	public static boolean isExecutable(final String filename) {
		for (String ext: EXECUTABLES) {
			if (filename.toLowerCase().endsWith(ext)) {
				return true;
			}
		}
		return false;
	}

	public static boolean isArchive(final String filename) {
		for (String ext: ARCHIVES) {
			if (filename.toLowerCase().endsWith(ext)) {
				return true;
			}
		}
		return false;
	}

	public static boolean isPhysFS(final String mountPath) {
		for (String ext: ARCHIVES) {
			if (mountPath.toLowerCase().endsWith(ext + ':' + File.separatorChar)) {
				return true;
			}
		}
		return false;
	}

	public static int containsPhysFS(final String mountPath) {
		for (String ext: ARCHIVES) {
			int idx = mountPath.toLowerCase().indexOf(ext + ':' + File.separatorChar);
			if (idx != -1) {
				return idx + ext.length();
			}
		}
		return -1;
	}

	public static boolean isFatImage(final String filename) {
		for (String ext: FATIMAGES) {
			if (filename.toLowerCase().endsWith(ext)) {
				return true;
			}
		}
		return false;
	}

	public static int containsFatImage(final String mountPath) {
		for (String ext: FATIMAGES) {
			int idx = mountPath.toLowerCase().indexOf(ext + File.separatorChar);
			if (idx != -1) {
				return idx + ext.length();
			}
		}
		return -1;
	}

	public static boolean isBooterImage(final String filename) {
		for (String ext: BOOTERIMAGES) {
			if (filename.toLowerCase().endsWith(ext)) {
				return true;
			}
		}
		return false;
	}

	public static boolean isConfFile(final String filename) {
		return filename.toLowerCase().endsWith(CONF_EXT);
	}

	public static boolean isGamePackArchiveFile(final String filename) {
		return filename.toLowerCase().endsWith(GAMEPACKARCHIVE_EXT);
	}

	public static boolean isIsoFile(final String filename) {
		for (String ext: CDIMAGES) {
			if (filename.toLowerCase().endsWith(ext)) {
				return true;
			}
		}
		return false;
	}

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

	public static boolean isPicture(final String filename) {
		for (String ext: PICTURES) {
			if (filename.toLowerCase().endsWith(ext)) {
				return true;
			}
		}
		return false;
	}

	public static int findSetupIndex(final java.util.List<File> files) {
		for (String setupFileName: SETUPFILES) {
			for (int i = 0; i < files.size(); i++) {
				if (files.get(i).getName().toLowerCase().equals(setupFileName))
					return i;
			}
		}
		return -1;
	}

	public static int findMostLikelyMainIndex(final String title, final java.util.List<File> files) {
		List<File> mostLikelyFiles = new ArrayList<File>();
		for (File f: files) {
			if (!Arrays.asList(SETUPFILES).contains(f.getName().toLowerCase()) && !Arrays.asList(UNLIKELYMAINFILES).contains(f.getName().toLowerCase())) {
				mostLikelyFiles.add(f);
			}
		}
		if (mostLikelyFiles.isEmpty())
			return StringRelatedUtils.findBestMatchIndex(title, getNames(files.toArray(new File[0])));
		return files.indexOf(mostLikelyFiles.get(StringRelatedUtils.findBestMatchIndex(title, getNames(mostLikelyFiles.toArray(new File[0])))));
	}

	public static File getCanMainFile(final File file) {
		String f = file.getPath();
		int isoIdx = containsIso(f);
		int pfsIdx = containsPhysFS(f);
		int fatIdx = containsFatImage(f);
		if (isoIdx != -1) {
			return canonicalToDosroot(f.substring(0, isoIdx));
		} else if (pfsIdx != -1) {
			return canonicalToDosroot(f.substring(0, pfsIdx));
		} else if (fatIdx != -1) {
			return canonicalToDosroot(f.substring(0, fatIdx));
		}
		return canonicalToDosroot(file.getPath());
	}

	public static List<File> getExecutablesInDirRecursive(final File dir) {
		List<File> executables = new ArrayList<File>(org.apache.commons.io.FileUtils.listFiles(dir, EXECUTABLES_UPPERCASE, true));
		Collections.sort(executables, new FileComparator());
		return executables;
	}

	public static String[] getExecutablesInZipOrIsoOrFat(final String archive) throws IOException {
		List<String> result = new ArrayList<String>();
		File arcFile = new File(archive);

		if (archive.toLowerCase().endsWith(ARCHIVES[0])) { // zip
			ZipFile zfile = new ZipFile(arcFile);
			for (Enumeration<? extends ZipEntry> entries = zfile.entries(); entries.hasMoreElements();) {
				ZipEntry entry = entries.nextElement();
				String name = entry.getName();
				if (!entry.isDirectory() && isExecutable(name)) {
					result.add(PlatformUtils.archiveToNativePath(name));
				}
			}
			zfile.close();
		} else if (archive.toLowerCase().endsWith(ARCHIVES[1])) { // 7-zip
			MyRandomAccessFile istream = new MyRandomAccessFile(archive, "r");
			IInArchive zArchive = new Handler();
			if (zArchive.Open(istream) != 0) {
				throw new IOException(Settings.getInstance().msg("general.error.opensevenzip", new Object[] {archive}));
			}
			for (int i = 0; i < zArchive.size(); i++) {
				SevenZipEntry entry = zArchive.getEntry(i);
				String name = entry.getName();
				if (!entry.isDirectory() && isExecutable(name)) {
					result.add(PlatformUtils.archiveToNativePath(name));
				}
			}
			zArchive.close();
		} else if (isIsoFile(archive)) {
			ISO9660FileSystem iso = new ISO9660FileSystem(new File(archive));
			for (Enumeration<ISO9660FileEntry> entries = iso.getEntries(); entries.hasMoreElements();) {
				ISO9660FileEntry entry = entries.nextElement();
				String name = entry.getPath();
				if (!entry.isDirectory() && isExecutable(name)) {
					result.add(PlatformUtils.archiveToNativePath(name));
				}
			}
			iso.close();
		} else if (isFatImage(archive)) {
			BlockDevice device = new FileDisk(new File(archive));
			result.addAll(readFatEntries(new FatFileSystem(device).getRoot(), ""));
			device.close();
		}

		Collections.sort(result, new FilenameComparator());
		return result.toArray(new String[result.size()]);
	}

	private static String fsDirectoryEntryNameToFileName(final String name) {
		if (name.length() > 8)
			return name.substring(0, 8).trim() + '.' + name.substring(8).trim();
		else
			return name;
	}

	private static List<String> readFatEntries(final FatLfnDirectory dir, final String dirPath) throws IOException {
		List<String> result = new ArrayList<String>();
		for (Iterator<FatLfnDirectoryEntry> entries = dir.iterator(); entries.hasNext();) {
			FatLfnDirectoryEntry entry = entries.next();
			String name = fsDirectoryEntryNameToFileName(entry.getShortName());
			if (entry.isDirectory() && !name.equals(".") && !name.equals("..")) {
				result.addAll(readFatEntries(entry.getDirectory(), dirPath + name + File.separatorChar));
			} else if (entry.isFile() && isExecutable(name)) {
				result.add(dirPath + name);
			}
		}
		return result;
	}

	public static void zipEntry(final File orgFile, final File fileEntry, final ZipOutputStream zos) throws IOException {
		ZipEntry anEntry = new ZipEntry(PlatformUtils.toArchivePath(fileEntry, orgFile.isDirectory()));
		anEntry.setTime(orgFile.lastModified());
		if (orgFile.isFile() && !orgFile.canWrite())
			anEntry.setExtra(new byte[] {1});
		zos.putNextEntry(anEntry);

		if (orgFile.isFile()) {
			byte[] readBuffer = new byte[ZIP_BUFFER];
			int bytes = 0;
			FileInputStream is = new FileInputStream(orgFile);
			while ((bytes = is.read(readBuffer)) != -1)
				zos.write(readBuffer, 0, bytes);
			is.close();
		}
		zos.closeEntry();
	}

	public static void zipDir(final File dirToZip, final ZipOutputStream zos, final File dstDirInZip) throws IOException {
		zipDir(dirToZip, zos, dirToZip, dstDirInZip);
	}

	private static void zipDir(final File dirToZip, final ZipOutputStream zos, final File zipRootDir, final File dstDirInZip) throws IOException {
		String[] dirList = dirToZip.list();
		if (dirList == null)
			throw new IOException(Settings.getInstance().msg("general.error.opendir", new Object[] {dirToZip}));
		for (int i = 0; i < dirList.length; i++) {
			File f = new File(dirToZip, dirList[i]);
			zipEntry(f, new File(dstDirInZip, makeRelativeTo(f, zipRootDir).getPath()), zos);
			if (f.isDirectory()) {
				zipDir(f, zos, zipRootDir, dstDirInZip);
			}
		}
	}

	public static long extractZipSizeInBytes(final File archive, final File dirToBeExtracted) throws IOException {
		long bytes = 0;
		ZipFile zf = new ZipFile(archive);
		for (Enumeration<? extends ZipEntry> entries = zf.entries(); entries.hasMoreElements();) {
			ZipEntry entry = entries.nextElement();
			if (areRelated(dirToBeExtracted, new File(entry.getName())))
				bytes += entry.getSize();
		}
		zf.close();
		return bytes;
	}

	public static long extractZipEntrySizeInBytes(final File archive, final String zipEntryToBeExtracted) throws IOException {
		ZipFile zf = new ZipFile(archive);
		ZipEntry entry = zf.getEntry(zipEntryToBeExtracted);
		zf.close();
		if (entry != null)
			return entry.getSize();
		return 0;
	}

	public static void extractZip(final File archive, final File dirToBeExtracted, final File dstDir, final ProgressNotifyable prog) throws IOException {
		ZipFile zf = new ZipFile(archive);
		for (Enumeration<? extends ZipEntry> entries = zf.entries(); entries.hasMoreElements();) {
			ZipEntry entry = entries.nextElement();
			if (areRelated(dirToBeExtracted, new File(entry.getName()))) {
				File dstFile = new File(dstDir, strip(new File(entry.getName()), dirToBeExtracted).getPath());
				extractEntry(zf, entry, dstFile, prog);
			}
		}
		zf.close();
	}

	public static void extractZip(final File archive, final String zipEntryToBeExtracted, final File dstFile, final ProgressNotifyable prog) throws IOException {
		ZipFile zf = new ZipFile(archive);
		ZipEntry entry = zf.getEntry(zipEntryToBeExtracted);
		if (entry != null)
			extractEntry(zf, entry, dstFile, prog);
		zf.close();
	}

	public static void extractEntry(final ZipFile zf, final ZipEntry srcEntry, final File dstFile, final ProgressNotifyable prog) throws IOException {
		File foundDstFile = null, temporarilyRenamedFile = null;
		if (PlatformUtils.IS_WINDOWS && dstFile.getName().contains("~")) {
			foundDstFile = dstFile.getCanonicalFile();
			if (!foundDstFile.getName().equals(dstFile.getName()) && foundDstFile.exists()) {
				temporarilyRenamedFile = getUniqueFileName(foundDstFile);
				foundDstFile.renameTo(temporarilyRenamedFile);
			}
		}

		if (dstFile.exists())
			throw new IOException(Settings.getInstance().msg("general.error.filetobeextractedexists", new Object[] {dstFile}));
		if (srcEntry.isDirectory()) {
			if (!dstFile.exists())
				createDir(dstFile);
		} else {
			if (dstFile.getParentFile() != null)
				createDir(dstFile.getParentFile());
			FileOutputStream fos = new FileOutputStream(dstFile);
			InputStream is = zf.getInputStream(srcEntry);
			byte[] readBuffer = new byte[ZIP_BUFFER];
			int bytesIn;
			while ((bytesIn = is.read(readBuffer)) != -1)
				fos.write(readBuffer, 0, bytesIn);
			fos.flush();
			fos.close();
			is.close();
			byte[] extra = srcEntry.getExtra();
			if ((extra != null) && (extra.length == 1) && (extra[0] == 1))
				fileSetReadOnly(dstFile);
		}
		fileSetLastModified(dstFile, srcEntry.getTime());

		if (foundDstFile != null && temporarilyRenamedFile != null)
			temporarilyRenamedFile.renameTo(foundDstFile);

		prog.incrProgress((int)(srcEntry.getSize() / 1024));
	}

	public static File determineDstSevenzipFile(File srcDir, File dstDir, String entryName) {
		return canonicalTo(dstDir, strip(new File(entryName), srcDir).getPath());
	}

	public static int[] findRelatedEntryIds(IInArchive zArchive, File dirToBeExtracted) {
		List<Integer> result = new ArrayList<Integer>();
		for (int i = 0; i < zArchive.size(); i++)
			if (areRelated(dirToBeExtracted, new File(zArchive.getEntry(i).getName())))
				result.add(i);
		return ArrayUtils.toPrimitive(result.toArray(new Integer[0]));
	}

	public static int findEntryId(IInArchive zArchive, String filenameToBeExtracted) {
		for (int i = 0; i < zArchive.size(); i++)
			if (zArchive.getEntry(i).getName().equals(filenameToBeExtracted))
				return i;
		return -1;
	}

	private static File strip(final File file, final File basePath) {
		if (file.equals(basePath))
			return new File(".");
		File remainder = new File(file.getName());
		File parent = file.getParentFile();
		while (parent != null) {
			if (parent.equals(basePath))
				return remainder;
			remainder = new File(parent.getName(), remainder.getPath());
			parent = parent.getParentFile();
		}
		return file;
	}

	public static String getUrlFromFile(final File file) throws MalformedURLException {
		return file.toURI().toURL().toString();
	}

	public static String fileSystemSafe(String s) {
		return s.replaceAll(INVALID_FILENAME_CHARS_REGEXP, "");
	}

	public static String fileSystemSafeWebImages(String s) {
		return s.replaceAll(" ", "_").replaceAll(INVALID_FILENAME_CHARS_REGEXP, "");
	}

	public static File getUniqueFileName(final File src) {
		return new File(src.getParentFile(), UUID.randomUUID() + "__" + src.getName());
	}

	public static File getTmpInstallFile() {
		return TMPINST_DIR_FILE;
	}

	public static boolean isStoredOnFloppyDrive(File file) {
		if (PlatformUtils.IS_OSX)
			return false; // FileSystemView doesn't work on OSX and leads to freeze on application exit

		FileSystemView fsv = FileSystemView.getFileSystemView();
		for (File f: File.listRoots()) {
			if (areRelated(f, file)) {
				return fsv.isFloppyDrive(f);
			}
		}
		return false;
	}

	public static boolean isStoredOnCDRomDrive(File file) {
		if (PlatformUtils.IS_OSX)
			return false; // FileSystemView doesn't work on OSX and leads to freeze on application exit

		FileSystemView fsv = FileSystemView.getFileSystemView();
		for (File f: File.listRoots()) {
			if (areRelated(f, file)) {
				return fsv.isDrive(f) && fsv.getSystemTypeDescription(f).toUpperCase().contains("CD");
			}
		}
		return false;
	}

	private static String[] getNames(File[] files) {
		String[] result = new String[files.length];
		for (int i = 0; i < files.length; i++)
			result[i] = files[i].getName();
		return result;
	}

	public static File[] findFileSequence(File f) {
		List<File> result = new ArrayList<File>();
		result.add(f);

		int i = 1;
		String name = FilenameUtils.removeExtension(f.getName());
		String ext = FilenameUtils.getExtension(f.getName());

		if (name.endsWith(String.valueOf(i))) {
			File dir = f.getParentFile();
			if (dir != null) {
				File[] files = dir.listFiles(new FileFilter() {
					public boolean accept(File f) {
						return f.isFile();
					}
				});
				String[] fileNames = getNames(files);
				if (files != null) {
					int index;
					do {
						i++;
						String nextFileName = StringUtils.chop(name) + String.valueOf(i) + FilenameUtils.EXTENSION_SEPARATOR + ext;
						index = ArrayUtils.indexOf(fileNames, nextFileName);
						if (index >= 0) {
							result.add(files[index]);
						}
					} while (index >= 0);
				}
			}
		}

		return result.toArray(new File[0]);
	}

	public static String[] getShaders() {
		File shadersDir = new File(DOSROOT_DIR_FILE, "SHADERS");
		if (shadersDir.exists() && shadersDir.isDirectory()) {
			List<String> result = new ArrayList<String>();
			Iterator<File> it = org.apache.commons.io.FileUtils.iterateFiles(shadersDir, new String[] {"fx"}, false);
			while (it.hasNext())
				result.add(it.next().getName());
			Collections.sort(result);
			return result.toArray(new String[0]);
		}
		return null;
	}

	public static Set<ShortFile> listShortFiles(File dir) {
		Set<ShortFile> result = new TreeSet<ShortFile>();
		for (File file: dir.listFiles())
			result.add(createShortFile(file, result));
		return result;
	}

	public static List<File> convertToShortFileSet(List<File> fileList) {
		Set<ShortFile> result = new TreeSet<ShortFile>();
		for (File file: fileList)
			result.add(createShortFile(file, result));
		List<File> res = new ArrayList<File>();
		for (ShortFile file: result)
			res.add(new File(file.getFile().getParentFile(), file.getName()));
		return res;
	}

	private static ShortFile createShortFile(File file, Set<ShortFile> curDir) {
		String filename = file.getName().toUpperCase();
		boolean createShort = false;
		if (StringUtils.contains(filename, ' ')) {
			filename = StringUtils.remove(filename, ' ');
			createShort = true;
		}
		int len = 0;
		int idx = filename.indexOf('.');
		if (idx != -1) {
			if (filename.length() - idx - 1 > 3) {
				filename = StringUtils.stripStart(filename, ".");
				createShort = true;
			}
			idx = filename.indexOf('.');
			len = (idx != -1) ? idx: filename.length();
		} else {
			len = filename.length();
		}
		createShort |= len > 8;

		ShortFile shortFile = null;
		if (!createShort) {
			shortFile = new ShortFile(file, StringUtils.removeEnd(filename, "."));
		} else {
			int i = 1;
			do {
				String nr = String.valueOf(i++);
				StringBuffer sb = new StringBuffer(StringUtils.left(filename, Math.min(8 - nr.length() - 1, len)));
				sb.append('~').append(nr);
				idx = filename.lastIndexOf('.');
				if (idx != -1)
					sb.append(StringUtils.left(filename.substring(idx), 4));
				shortFile = new ShortFile(file, StringUtils.removeEnd(sb.toString(), "."));
			} while (shortFile.isContainedIn(curDir));
		}
		return shortFile;
	}
}
