package cx.fbn.nevernote.config;

import java.io.File;
import java.io.FileFilter;
import java.util.regex.Pattern;

/**
 * Provides access to NeverNote standard runtime directories.
 *
 * @author Nick Clarke
 */
public class FileManager {

    private static final Pattern ALL_PATH_SEPARATORS_REGEX = Pattern.compile("[/\\\\]");

    private final String programDirPath;
    private final File programDir;
    
    private final String homeDirPath;
    private final File homeDir;

    private final String dbDirPath;
    private final File dbDir;

    private final File logsDir;

    private final String imagesDirPath;
    private final File imagesDir;

    private final String spellDirPath;
    private final File spellDir;
    
    private final String spellDirPathUser;
    private final File spellDirUser;
    
    private final String qssDirPath;
    private final File qssDir;
    
    private final String qssDirPathUser;
    private final File qssDirUser;

    private final String resDirPath;
    private final File resDir;

    private final File xmlDir;

    private final String translateDirPath;
    private final File translateDir;

    /**
     * Check or create the db, log and res directories.
     *
     * @param homeDirPath the installation dir containing db/log/res directories, must exist
     * @throws InitializationException for missing directories or file permissions problems
     */
    public FileManager(String homeDirPath, String programDirPath) throws InitializationException {
        if (homeDirPath == null) {
            throw new IllegalArgumentException("homeDirPath must not be null");
        }
        if (programDirPath == null) {
            throw new IllegalArgumentException("programDirPath must not be null");
        }

        this.homeDir = new File(toPlatformPathSeparator(homeDirPath));
        this.programDir = new File(toPlatformPathSeparator(programDirPath));
        createDirOrCheckWriteable(homeDir);
        this.homeDirPath = slashTerminatePath(homeDir.getPath());
        this.programDirPath = slashTerminatePath(programDir.getPath());
        
        // Read-only
        imagesDir = new File(programDir, "images");
        checkExistingReadableDir(imagesDir);
        imagesDirPath = slashTerminatePath(imagesDir.getPath());

        qssDir = new File(programDir, "qss");
        checkExistingReadableDir(qssDir);
        qssDirPath = slashTerminatePath(qssDir.getPath());
        
        qssDirUser = new File(homeDir, "qss");
        createDirOrCheckWriteable(qssDirUser);
        qssDirPathUser = slashTerminatePath(qssDirUser.getPath());

        spellDir = new File(programDir, "spell");
        checkExistingReadableDir(spellDir);
        spellDirPath = slashTerminatePath(spellDir.getPath());
        

        spellDirUser = new File(homeDir, "spell");
        createDirOrCheckWriteable(spellDirUser);
        spellDirPathUser = slashTerminatePath(spellDirUser.getPath());
        
        xmlDir = new File(programDir, "xml");
        checkExistingReadableDir(xmlDir);

        translateDir = new File(programDir, "translations");
        checkExistingReadableDir(translateDir);
        translateDirPath= slashTerminatePath(translateDir.getPath());

        // Read-write
        dbDir = new File(homeDir, "db");
        createDirOrCheckWriteable(dbDir);
        dbDirPath = slashTerminatePath(dbDir.getPath());

        logsDir = new File(homeDir, "logs");
        createDirOrCheckWriteable(logsDir);

        resDir = new File(homeDir, "res");
        createDirOrCheckWriteable(resDir);
        resDirPath = slashTerminatePath(resDir.getPath());
    }

    /**
     * Get a file below the base user home directory.
     */
    public File getProgramDirFile(String relativePath) {
        return new File(programDir, toPlatformPathSeparator(relativePath));
    }

    /**
     * Get a path below the base user home directory, using native {@link File#separator}.
     * This will contain backslashes on Windows.
     */
    public String getProgramDirPath(String relativePath) {
        return programDirPath + toPlatformPathSeparator(relativePath);
    }
    
    /**
     * Get a file below the base user home directory.
     */
    public File getHomeDirFile(String relativePath) {
        return new File(homeDir, toPlatformPathSeparator(relativePath));
    }

    /**
     * Get a path below the base user home directory, using native {@link File#separator}.
     * This will contain backslashes on Windows.
     */
    public String getHomeDirPath(String relativePath) {
        return homeDirPath + toPlatformPathSeparator(relativePath);
    }

    /**
     * Get a file below the 'db' directory.
     */
    public File getDbDirFile(String relativePath) {
        return new File(dbDir, toPlatformPathSeparator(relativePath));
    }

    /**
     * Get a path below the 'spell' directory, using native {@link File#separator}.
     * This will contain backslashes on Windows.
     */
    public String getSpellDirPath(String relativePath) {
        return dbDirPath + toPlatformPathSeparator(relativePath);
    }

    /**
     * Get a file below the 'spell' directory.
     */
    public File getSpellDirFile(String relativePath) {
        return new File(spellDir, toPlatformPathSeparator(relativePath));
    }
    
    /** 
     * Get the spell directory for the jazzy word list
     */
    public String getSpellDirPath() {
    	return spellDirPath;
    }

    /**
     * Get a file below the 'spell' directory for user dictionaries.
     */
    public File getSpellDirFileUser(String relativePath) {
        return new File(spellDirUser, toPlatformPathSeparator(relativePath));
    }
    
    /** 
     * Get the spell directory for the jazzy word list (user dictionary).
     */
    public String getSpellDirPathUser() {
    	return spellDirPathUser;
    }
    
    /**
     * Get a path below the 'db' directory, using native {@link File#separator}.
     * This will contain backslashes on Windows.
     */    
    public String getDbDirPath(String relativePath) {
        return dbDirPath + toPlatformPathSeparator(relativePath);
    }
    
    /**
     * Get a file below the 'images' directory.
     */
    public File getImageDirFile(String relativePath) {
        return new File(imagesDir, toPlatformPathSeparator(relativePath));
    }

    /**
     * Get a path below the 'images' directory, using native {@link File#separator}.
     * This will contain backslashes on Windows.
     */
    public String getImageDirPath(String relativePath) {
        return imagesDirPath + toPlatformPathSeparator(relativePath);
    }

    /**
     * Get a file below the 'logs' directory.
     */
    public File getLogsDirFile(String relativePath) {
        return new File(logsDir, toPlatformPathSeparator(relativePath));
    }

    /**
     * Get a path below the 'qss' directory, using native {@link File#separator}.
     * This will contain backslashes on Windows.
     */
    public String getQssDirPath(String relativePath) {
        return qssDirPath + toPlatformPathSeparator(relativePath);
    }

    /**
     * Get a path below the 'qss' directory, using native {@link File#separator}.
     * This will contain backslashes on Windows.
     */
    public String getQssDirPathUser(String relativePath) {
        return qssDirPathUser + toPlatformPathSeparator(relativePath);
    }
    
    /**
     * Get a path to the 'res' directory, terminated with native {@link File#separator}.
     * This will contain backslashes on Windows.
     */
    public String getResDirPath() {
        return resDirPath;
    }

    /**
     * Get a path below the 'res' directory, using native {@link File#separator}.
     * This will contain backslashes on Windows.
     */
    public String getResDirPath(String relativePath) {
        return resDirPath + toPlatformPathSeparator(relativePath);
    }
    
    /**
     * Get a path below the 'res' directory, using native {@link File#separator}.
     * This will contain backslashes on Windows.  This is different from the 
     * one above in that it will encode the relative path
     */
    public String getResDirPathSpecialChar(String relativePath) {
   		return resDirPath + toPlatformPathSeparator(relativePath).replace("#", "%23");
    }

    /**
     * Get a file below the 'xml' directory.
     */
    public File getXMLDirFile(String relativePath) {
        return new File(xmlDir, toPlatformPathSeparator(relativePath));
    }

    /**
     * Get a path below the 'translate' directory, using native {@link File#separator}.
     * This will contain backslashes on Windows.
     */
    public String getTranslateFilePath(String relativePath) {
        return translateDirPath + toPlatformPathSeparator(relativePath);
    }

    public static String toPlatformPathSeparator(String relativePath) {
    	// Sometimes a space in the file name comes across as a %20.  This is to put it back as a space.
    	relativePath = relativePath.replace("%20", " ");
    	return ALL_PATH_SEPARATORS_REGEX.matcher(relativePath).replaceAll(
				// Must double-escape backslashes,
				// because they have special meaning in the replacement string of Matcher.replaceAll
				(File.separator.equals("\\") ? "\\\\" : File.separator));
    }

    public static String slashTerminatePath(String path) {
        if (!path.substring(path.length() - 1).equals(File.separator)) {
            return path + File.separator;
        }
        return path;
    }

    /**
     * Delete first-level files (but not directories) from the directory.
     *
     * @throws InitializationException for file deletion failures
     */
    private static void deleteTopLevelFiles(File dir, boolean exitOnFail) throws InitializationException {
        File[] toDelete = dir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isFile();
            }
        });
        for (File f : toDelete) {
            if (!f.delete() && exitOnFail) {
                throw new InitializationException("Failed to delete file: '" + f + "'");
            }
        }
    }

    /**
     * @throws InitializationException for bad file permissions, or a file instead of a directory
     */
    private static void createDirOrCheckWriteable(File dir) throws InitializationException {
        if (dir.isDirectory()) {
            // Dir exists, check permissions
            if (!dir.canRead()) {
                throw new InitializationException("Directory '" + dir + "' does not have read permission");
            }
            if (!dir.canWrite()) {
                throw new InitializationException("Directory '" + dir + "' does not have write permission");
            }
        } else if (!dir.exists()) {
            if (!dir.mkdirs()) {
                throw new InitializationException("Failed to create directory '" + dir + "'");
            }
        } else {
            throw new InitializationException("Expected directory '" + dir + "' but found a file instead");
        }
    }

    /**
     * @throws InitializationException if non-existent, bad file permissions, or a file instead of a directory
     */
    private static void checkExistingReadableDir(File dir) throws InitializationException {
        if (dir.isDirectory()) {
            // Dir exists, check permissions
            if (!dir.canRead()) {
                throw new InitializationException("Directory '" + dir + "' does not have read permission");
            }
        } else if (!dir.exists()) {
            throw new InitializationException("Directory '" + dir + "' does not exist");
        } else {
            throw new InitializationException("Expected directory '" + dir + "' but found a file instead");
        }
    }

    /**
     * @throws InitializationException if non-existent, bad file permissions, or a file instead of a directory
     */
    @SuppressWarnings("unused")
	private static void checkExistingWriteableDir(File dir) throws InitializationException {
        checkExistingReadableDir(dir);
        if (!dir.canWrite()) {
            throw new InitializationException("Directory '" + dir + "' does not have write permission");
        }
    }

    /**
     * Called at startup to purge files from 'res' directory.
     */
    public void purgeResDirectory(boolean exitOnFail) throws InitializationException {
        deleteTopLevelFiles(resDir, exitOnFail);
    }
}
