/*
 *  Copyright (C) 2006-2015  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.nio.channels.FileChannel;
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;
    private 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 void copy(final File source, final File dest) {
        FileChannel inStream = null, outStream = null;
        try {          
            inStream = new FileInputStream(source).getChannel();
            outStream = new FileOutputStream(dest).getChannel();
            inStream.transferTo(0, inStream.size(), outStream);
        } catch (IOException e) {
            System.err.println(Settings.getInstance().msg("general.error.copyfile", new Object[] {source, dest}));
        } finally {
            if (inStream != null) { 
                try {
                    inStream.close();
                } catch (IOException ex) {
                    System.err.println(Settings.getInstance().msg("general.error.closefileinputstream", new Object[] {source}));
                }
            }
            if (outStream != null) {
                try {
                    outStream.close();
                } catch (IOException ex) {
                    System.err.println(Settings.getInstance().msg("general.error.closefileoutputstream", new Object[] {source}));
                }
            }
        }
    }
    
    private static List<String> initCommands(final DosboxVersion dbversion, boolean forceDBConf) {
    	List<String> execCommands = new ArrayList<String>();
    	if (dbversion.isUsingCurses() && PlatformUtils.IS_WINDOWS) {
    		execCommands.add("rundll32");
    		execCommands.add("SHELL32.DLL,ShellExec_RunDLL");
        }
    	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 List<String> execCommands, final File cwd,
    		Map<String, String> env, final boolean waitFor) throws IOException {
        StringBuffer cmd = new StringBuffer();
        try {
            String[] rtCmds = new String[execCommands.size()];
            for (int i = 0; i < execCommands.size(); i++) {
                rtCmds[i] = execCommands.get(i);
                cmd.append(rtCmds[i]).append(' ');
            }
            System.out.print(cmd);
            File dir = (cwd == null)? DOSROOT_DIR_FILE: cwd;
            ProcessBuilder pb = new ProcessBuilder(rtCmds);
            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(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 {
        doRunDosbox(dbversion, new String[] {"-c", "config -writeconf '" + dbversion.getCanonicalConfFile() + "'", "-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(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(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());
                    copy(src, dst);
                    fileSetLastModified(dst, src.lastModified());
                }
            }
        }
    }

	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;
	}
}
