/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */

package com.queplix.core.modules.eqlext.utils;

import com.queplix.core.error.GenericSystemException;
import com.queplix.core.modules.config.utils.SysPropertyManager;
import com.queplix.core.modules.eqlext.error.FileManagerException;
import com.queplix.core.utils.FileHelper;
import com.queplix.core.utils.StringHelper;
import com.queplix.core.utils.log.AbstractLogger;
import com.queplix.core.utils.log.Log;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * Attachment's file and directory helper class.
 *
 * @author [ALB] Andrey Baranov
 * @author [DBR] Daniel Raskin
 * @author [ONZ] Oleg N. Zhovtanyuk
 * @version $Revision: 1.1.1.1 $ $Date: 2005/09/12 15:30:41 $
 * @see com.queplix.core.utils.FileHelper
 */

public final class FileManager {

    // ========================================================== Initialization

    // Name separator
    public static final char NAME_SEPARATOR = '/';

    // Logger instance.
    private static final AbstractLogger logger = Log.getLog(FileManager.class);

    // Private constructor - blocks instantiation.
    private FileManager() {
    }

    // ========================================== Directories management methods

    /**
     * Returns the main file storage directory.
     *
     * @return File object
     */
    public static File getFSDirectory() {
        try {
            String dirName = SysPropertyManager.getProperty("FILE_STORAGE");
            File fsDir = new File(dirName);
            if(!fsDir.exists()) {
                fsDir.mkdirs();
            }
            return fsDir.getCanonicalFile();
        } catch (IOException ex) {
            throw new GenericSystemException(ex);
        }
    }

    /**
     * Gets the directory (as the <code>File</code> object) by the name.
     *
     * @param dirName LOCAL directory name
     * @return File object or null, if not found
     * @throws FileManagerException
     */
    public static File getDirectory(String dirName)
            throws FileManagerException {

        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("Getting directory '" + dirName + "'...");
        }

        // Check for empty name.
        if(StringHelper.isEmpty(dirName)) {
            throw new NullPointerException("Directory name is empty.");
        }

        File dir;
        try {
            dir = getFullFile(dirName);
            String fullName = dir.getAbsolutePath();

            if(!checkFile(dir)) {
                throw new java.lang.SecurityException(
                        "Access denied to file '" + fullName + "'.");
            }

            if(!dir.exists()) {
                logger.WARN("Directory '" + fullName + "' does not exist.");
                return null;
            }

        } catch (java.lang.SecurityException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);
        }

        // Ok.
        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("Got directory '" + dir + "'.");
        }
        return dir;
    }

    /**
     * Creates a new directory.
     *
     * @param dirName LOCAL directory name
     * @return <b>true</b> on success, <b>false</b> on fail
     * @throws FileManagerException
     */
    public static boolean createDirectory(String dirName)
            throws FileManagerException {

        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("Creating directory '" + dirName + "'...");
        }

        // Check for empty name.
        if(StringHelper.isEmpty(dirName)) {
            throw new NullPointerException("Directory name is empty.");
        }

        File dir;
        try {
            dir = getFullFile(dirName);

            if(!checkFile(dir)) {
                throw new java.lang.SecurityException(
                        "Can't create directory '" +
                                dir.getAbsolutePath() + "' - access denied.");
            }

            FileHelper.createDirectory(dir);

        } catch (IOException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);

        } catch (java.lang.SecurityException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);
        }

        // Ok.
        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("Directory '" + dir + "' created.");
        }
        return true;

    }

    /**
     * Rename the existing directory.
     *
     * @param oldDirName old LOCAL directory name
     * @param dirName    new LOCAL directory name
     * @return <b>true</b> on success, <b>false</b> on fail
     * @throws FileManagerException
     */
    public static boolean renameDirectory(String oldDirName, String dirName)
            throws FileManagerException {

        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("Renaming directory '" + oldDirName + "' to '" + dirName
                    + "'...");
        }

        // Check for empty names.
        if(StringHelper.isEmpty(oldDirName)) {
            throw new NullPointerException("The old directory name is empty.");
        }
        if(StringHelper.isEmpty(dirName)) {
            throw new NullPointerException("The new directory name is empty.");
        }

        File oldDir;
        File dir;
        try {
            oldDir = getFullFile(oldDirName);
            dir = getFullFile(dirName);

            // If directories are similar - skip process
            if(oldDir.equals(dir)) {
                return false;
            }

            String oldFullName = oldDir.getAbsolutePath();
            String fullName = dir.getAbsolutePath();

            if(!oldDir.exists()) {
                throw new FileNotFoundException("Can't rename directory '" +
                        oldFullName + "' - it doesn't exist.");
            }

            if(!oldDir.isDirectory()) {
                throw new FileNotFoundException("Can't rename '" + oldFullName +
                        "' - it is not a directory.");
            }

            if(!checkFile(dir)) {
                throw new java.lang.SecurityException(
                        "Can't rename to directory '" +
                                fullName + "' - access denied.");
            }

            // Rename dir.
            /** @todo move it to FileHelper in future */
            if(!oldDir.renameTo(dir)) {
                throw new IOException("Can't rename directory '" + oldFullName +
                        "' to '" + fullName + "' - internal I/O error.");
            }

        } catch (IOException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);

        } catch (java.lang.SecurityException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);
        }

        // Ok.
        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO(
                    "Directory '" + oldDir + "' is renamed to '" + dir + "'.");
        }
        return true;
    }

    /**
     * Deletes the existing directory.
     *
     * @param dirName LOCAL directory name
     * @return <b>true</b> on success, <b>false</b> on fail
     * @throws FileManagerException
     */
    public static boolean deleteDirectory(String dirName)
            throws FileManagerException {

        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("Deleting directory '" + dirName + "'...");
        }

        // Check for empty name.
        if(StringHelper.isEmpty(dirName)) {
            throw new NullPointerException("Directory name is empty.");
        }

        File dir;
        try {
            dir = getFullFile(dirName);
            if(!dir.exists()) {
                logger.WARN("Directory '" + dirName + "' doesn't exist.");
                return false;
            }

            String fullName = dir.getAbsolutePath();
            if(!dir.isDirectory()) {
                throw new FileNotFoundException("Can't delete '" + fullName +
                        "' - it is not a directory.");
            }

            String[] listing = dir.list();
            if(listing != null && listing.length > 0) {
                throw new IOException("Can't delete directory '" + fullName +
                        "' - it is not empty");
            }

            if(!checkFile(dir)) {
                throw new java.lang.SecurityException(
                        "Can't delete directory '" +
                                fullName + "' - access denied.");
            }

            // Delete directory.
            if(!FileHelper.delete(dir)) {
                throw new IOException("Can't delete directory '" + fullName +
                        "' - internal I/O error.");
            }

        } catch (IOException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);

        } catch (java.lang.SecurityException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);
        }

        // Ok.
        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("Directory '" + dir + "' deleted.");
        }
        return true;
    }

    // ================================================ Files management methods

    /**
     * Loads the file by name (as the <code>FileInfo</code> object).
     *
     * @param filePath LOCAL file path
     * @return FileInfo object
     * @throws FileManagerException
     */
    public static FileInfo loadFile(String filePath)
            throws FileManagerException {

        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("Loading file '" + filePath + "'...");
        }

        // Check for empty path.
        if(StringHelper.isEmpty(filePath)) {
            throw new NullPointerException("File path is empty.");
        }

        FileInfo fi;
        try {
            File fullFile = getFullFile(filePath);

            if(logger.getLogger().isDebugEnabled()) {
                logger.DEBUG("Full path: " + fullFile.getCanonicalPath());
            }

            byte[] buf = FileHelper.loadFile(fullFile);
            fi = new FileInfo(filePath, buf);

        } catch (IOException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);

        } catch (java.lang.SecurityException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);
        }

        // Ok.
        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("File '" + filePath + "' loaded.");
            logger.INFO("Loaded " + fi.getData().length + " bytes");
        }
        return fi;
    }

    /**
     * <p>Copy file with LOCAL path <code>srcFilePath</code> to the
     * destination file <code>destFile</code>.</p>
     * <p>Don't check access permissions for <code>destFile</code>.</p>
     * <p>Create all subdirectories if needed.</p>
     *
     * @param srcFilePath given LOCAL file path
     * @param destFile    destination file
     * @throws FileManagerException
     */
    public static void copyFile(String srcFilePath, File destFile)
            throws FileManagerException {

        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("Copying file '" + srcFilePath + "'...");
        }

        // Check for emptiness.
        if(StringHelper.isEmpty(srcFilePath)) {
            throw new NullPointerException("Soyrce file path is empty.");
        }
        if(destFile == null) {
            throw new NullPointerException("Destination file is null");
        }

        try {
            // Load file
            FileInfo fi = loadFile(srcFilePath);

            // Create all subdirectories.
            File newDir = destFile.getParentFile();
            FileHelper.createDirectory(newDir);

            // Save as new
            writeFile(destFile, fi);

        } catch (IOException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);

        } catch (java.lang.SecurityException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);
        }

        // Ok.
        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("File '" + srcFilePath + "' copied to '" +
                    destFile.getAbsolutePath() + "'.");
        }
    }

    /**
     * Saves the file.
     *
     * @param fileInfo  FileInfo object
     * @param subFolder LOCAL subfolder to store (optional)
     * @return File object
     * @throws FileManagerException
     */
    public static File saveFile(FileInfo fileInfo, String subFolder)
            throws FileManagerException {

        // Emtpiness checks.
        if(fileInfo == null) {
            throw new NullPointerException("File info is null.");
        }
        String fileName = fileInfo.getFilePath();
        if(StringHelper.isEmpty(fileName)) {
            throw new NullPointerException("File name is empty.");
        }
        if(subFolder == null) {
            subFolder = StringHelper.EMPTY_VALUE;
        }

        // Get file name.
        fileName = strip(fileName);
        int pos = fileName.lastIndexOf(NAME_SEPARATOR);
        if(pos >= 0) {
            fileName = fileName.substring(pos + 1);
        }

        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("File name: '" + fileName + "'.");
            logger.INFO("Subfolder name: '" + subFolder + "'.");
        }

        File file = null;
        try {

            // Create local directory if it's not empty and get its full name.
            if(!StringHelper.isEmpty(subFolder)) {
                createDirectory(subFolder);
            }
            File fileDirectory = getFullFile(subFolder);

            // Rename, if the file already exists.
            file = new File(fileDirectory, fileName);
            if(file.exists()) {

                // Get file extension.
                String _fileName = fileName;
                String _fileExt = "";
                int k = fileName.lastIndexOf('.');
                if(k >= 0) {
                    _fileName = fileName.substring(0, k);
                    _fileExt = fileName.substring(k);
                }

                // File exists, modify file name by adding "_2", "_3", ...
                long i = 2;
                do {
                    String newFileName = _fileName + "_" + (i++) + _fileExt;
                    file = new File(fileDirectory, newFileName);
                } while(file.exists() && i < Integer.MAX_VALUE);

            }

            if(!checkFile(file)) {
                throw new java.lang.SecurityException("Can't save file '" +
                        file.getAbsolutePath() + "' - access denied.");
            }

            // Write file on the disk.
            writeFile(file, fileInfo);

        } catch (IOException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);

        } catch (java.lang.SecurityException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);
        }

        // Ok.
        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("File '" + file + "' saved.");
        }
        return file;
    }

    /**
     * Replaces the existing file.
     *
     * @param filePath LOCAL file path
     * @param fileInfo FileInfo object
     * @return File object of new file
     * @throws FileManagerException
     */
    public static File replaceFile(String filePath, FileInfo fileInfo)
            throws FileManagerException {

        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("Replacing the file '" + filePath + "' ...");
        }

        // Emptiness check.
        if(StringHelper.isEmpty(filePath)) {
            throw new NullPointerException("File path is empty.");
        }
        if(fileInfo == null) {
            throw new NullPointerException("File info is null.");
        }

        File file = null;
        try {

            file = getFullFile(filePath);
            if(!checkFile(file)) {
                throw new java.lang.SecurityException("Can't replace file '" +
                        file.getAbsolutePath() + "' - access denied.");
            }

            // Overwrite the file on disk.
            writeFile(file, fileInfo);

        } catch (IOException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);

        } catch (java.lang.SecurityException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);
        }

        // Ok.
        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("File '" + file + "' replaced.");
        }
        return file;
    }

    /**
     * Deletes the existing file.
     *
     * @param filePath LOCAL file path
     * @throws FileManagerException
     */
    public static void deleteFile(String filePath)
            throws FileManagerException {

        if(logger.getLogger().isInfoEnabled()) {
            logger.INFO("Deleting the file '" + filePath + "' ...");
        }

        // Check for empty path.
        if(StringHelper.isEmpty(filePath)) {
            throw new NullPointerException("File path is empty.");
        }

        try {

            File file = getFullFile(filePath);
            if(!checkFile(file)) {
                throw new java.lang.SecurityException("Can't delete file '" +
                        file.getAbsolutePath() + "' - access denied.");
            }

            // Delete the file if it exists.
            FileHelper.deleteFile(file);

        } catch (IOException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);

        } catch (java.lang.SecurityException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);
        }
    }

    /**
     * Gets the LOCAL (AKA relative to upload directory) name for the
     * given LOCAL directiry name and LOCAL file name.
     *
     * @param fileName  LOCAL file name
     * @param subFolder LOCAL subfolder (optional)
     * @return LOCAL name (subFolder + NAME_SEPARATOR + fileName)
     * @throws FileManagerException
     */
    public static String getLocalName(String fileName, String subFolder)
            throws FileManagerException {

        // Check for emptiness.
        if(StringHelper.isEmpty(fileName)) {
            throw new NullPointerException("File name is empty.");
        }

        // Ok.
        if(StringHelper.isEmpty(subFolder)) {
            return fileName;
        } else {
            return subFolder + NAME_SEPARATOR + fileName;
        }
    }

    /**
     * Gets the LOCAL name for the given file.
     *
     * @param file given File object
     * @return LOCAL name
     * @throws FileManagerException
     */
    public static String getLocalName(File file)
            throws FileManagerException {

        // Check file.
        if(file == null) {
            throw new NullPointerException("File is null.");
        }

        try {
            String fileAbsPath = file.getCanonicalPath();
            String fsdirAbsPath = getFSDirectory().getCanonicalPath();

            if(!fileAbsPath.startsWith(fsdirAbsPath)) {
                throw new IllegalStateException("File '" + fileAbsPath +
                        "' is not under the upload directory.");
            }

            String filePath = strip(fileAbsPath.substring(
                    fsdirAbsPath.length()));
            if(filePath.indexOf(NAME_SEPARATOR) == 0) {
                filePath = filePath.substring(1);
            }

            return filePath;

        } catch (IOException ex) {
            logger.ERROR(ex);
            throw new FileManagerException(ex);
        }

    }

    /**
     * Builds system independent file name.
     *
     * @param s some file name
     * @return system file name
     */
    public static String strip(String s) {
        return s.replace('\\', NAME_SEPARATOR);
    }

    //
    // Gets the full name of the given file.
    //
    public static File getFullFile(String filePath) {
        if(StringHelper.isEmpty(filePath)) {
            return getFSDirectory();
        } else {
            return new File(getFSDirectory(), strip(filePath));
        }
    }

    //
    // Gets the file with the full name.
    //
    public static File getFullFile(String dirPath, String filePath) {
        return new File(getFullFile(dirPath), strip(filePath));
    }

    // ========================================================= Private methods

    //
    // Writes file to disk.
    //

    private static void writeFile(File file, FileInfo fileInfo)
            throws IOException {
        FileHelper.writeFile(file, fileInfo.getData());
    }

    //
    // Checks if the given file is under the file storage directory.
    //
    private static boolean checkFile(File file) {
        try {
            getLocalName(file);
            return true;
        } catch (Exception ex) {
            logger.ERROR(ex);
        }
        return false;
    }

    // =========================================================== Inner classes

    /**
     * Stores the file information.
     *
     * @author [ALB] Baranov Andrey
     * @version 1.0
     */
    public static final class FileInfo {

        private String filePath;
        private byte[] data;

        public FileInfo(String filePath, byte[] data) {
            this.filePath = filePath;
            this.data = data;
        }

        public String getFilePath() {
            return filePath;
        }

        public byte[] getData() {
            return data;
        }
    }

}
