/*
 *  Copyright (C) 2006-2012  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.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.StreamGobbler;
import org.dbgl.loopy.iso9660.ISO9660FileEntry;
import org.dbgl.loopy.iso9660.ISO9660FileSystem;
import org.dbgl.model.DosboxVersion;
import org.dbgl.model.Profile;
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 SevenZip.MyRandomAccessFile;
import SevenZip.Archive.IInArchive;
import SevenZip.Archive.SevenZipEntry;
import SevenZip.Archive.SevenZip.Handler;


public final class FileUtils {

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

    private 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 Settings settings = Settings.getInstance();
        final String DATADIR = getCanonicalPath(settings.getSettings().getValue("directory", "data"));
        DATA_DIR_FILE = new File(DATADIR);
        DOSROOT_DIR_FILE = new File(DATADIR, DOSROOT_DIR);
        final String DOSBOXDIR = getCanonicalPath(settings.getSettings().getValue("directory", "dosbox"));
        DOSBOX_DIR_FILE = new File(DOSBOXDIR);
        TMPINST_DIR_FILE = new File(DOSROOT_DIR_FILE, settings.getSettings().getValue("directory", "tmpinstall"));

        createDirIfNecessary(DOSROOT_DIR_FILE);
    }

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

    public static boolean isExistingFile(final File file) {
        return file.isFile() && file.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 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 constructRelativeCapturesDir(final int profileId) {
        return ".." + File.separatorChar + constructCapturesDir(profileId);
    }

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

    public static String constructUniqueConfigFileString(final int profileId, final String profileTitle, final File canonicalMainDir) {
        SectionsWrapper set = Settings.getInstance().getSettings();
        File path = (set.getIntValue("profiledefaults", "confpath") == 0)? new File(PROFILES_DIR): 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(FileUtils.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) throws IOException {
        DosboxVersion dbv = DosboxVersion.findById(dbversions, prof.getDbversionId());
        doRunProfile(prof, dbv, env, setup, prepareOnly);
    }

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

    public static void doRunProfile(final Profile prof, final DosboxVersion dbversion, final Map<String, String> env, final int setup, final boolean prepareOnly) throws IOException {
        File file = prof.getCanonicalConfFile();
        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);
        }
        doRunDosbox(dbversion, new String[] {"-conf", file.getPath()}, false, null, env, false);
    }
    
    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, Map<String, String> env) throws IOException {
    	File file = canonicalToData(PROFILES_DIR + TEMPLATE_CONF);
        Conf conf = new Conf(template, file, dbversion, System.err);
        conf.save();
        doRunDosbox(dbversion, new String[] {"-conf", file.getPath()}, false, null, env, false);
    }
  
    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 = canonicalTo(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();
    }

    private static void extractEntry(final ZipFile zf, final ZipEntry srcEntry, final File dstFile, final ProgressNotifyable prog) throws IOException {
    	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[0] == 1)) fileSetReadOnly(dstFile);
    	}
    	fileSetLastModified(dstFile, srcEntry.getTime());
    	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 (FileUtils.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 getTmpInstallFile() {
    	return TMPINST_DIR_FILE;
    }

    public static boolean isStoredOnFloppyDrive(File file) {
    	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) {
    	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]);
    }
}
