/*
 * 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.utils.net;

import com.queplix.core.utils.StringHelper;
import com.queplix.core.utils.log.AbstractLogger;
import com.queplix.core.utils.log.Log;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

/**
 * Provides basic FTP client operations.
 *
 * @author [DBR] Daniel Raskin
 * @author [ALB] Baranov Andrey
 * @author [ONZ] Oleg N. Zhovtanyuk
 * @version $Revision: 1.2 $ $Date: 2006/05/03 15:35:14 $
 */
public class FTPHelper {

    // =================================================== Constants and fields

    /**
     * Anonymous login.
     */
    public static final String ANONYMOUS_LOGIN = "anonymous";

    private static final AbstractLogger logger = Log.getLog(FTPHelper.class);
    private FTPClient ftp;
    private boolean is_connected;

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

    /**
     * Creates a new FTP client instance.
     */
    public FTPHelper() {
        ftp = new FTPClient();
        is_connected = false;
    }

    // ========================================================= Public methods

    /**
     * Connects to FTP server.
     *
     * @param host       FTP server address or name
     * @param user       user name
     * @param password   user password
     * @param isTextMode text / binary mode switch
     * @throws IOException on I/O errors
     */
    public void connect(String host, String user, String password,
                        boolean isTextMode)
            throws IOException {

        // Connect to server.
        logger.DEBUG("Connecting to FTP host '" + host + "'...");
        try {
            ftp.connect(host);
        } catch (UnknownHostException ex) {
            throw new IOException("Can't find FTP server '" + host + "'");
        }

        // Check rsponse after connection attempt.
        int reply = ftp.getReplyCode();
        if(!FTPReply.isPositiveCompletion(reply)) {
            disconnect();
            throw new IOException("Can't connect to server '" + host + "'");
        }

        if(StringHelper.isEmpty(user)) {
            user = ANONYMOUS_LOGIN;
        }

        // Login.
        logger.DEBUG("Logging in as '" + user + "'...");
        if(!ftp.login(user, password)) {
            is_connected = false;
            disconnect();
            throw new IOException("Can't login to server '" + host + "'");
        } else {
            is_connected = true;
        }

        // Set data transfer mode.
        if(isTextMode) {
            ftp.setFileType(FTP.ASCII_FILE_TYPE);
        } else {
            ftp.setFileType(FTP.BINARY_FILE_TYPE);
        }
        logger.DEBUG("Data transfer mode is set to " + (isTextMode ? "text."
                :"binary."));

    }

    /**
     * Uploads the file to the FTP server.
     *
     * @param ftpFileName server file name (with absolute path)
     * @param localFile   local file to upload
     * @throws IOException on I/O errors
     */
    public void upload(String ftpFileName, File localFile)
            throws IOException {

        logger.DEBUG("FTP upload: " + localFile + " -> " + ftpFileName);

        // File check.
        if(!localFile.exists()) {
            throw new IOException("Can't upload '"
                    + localFile.getAbsolutePath()
                    + "'. This file doesn't exist.");
        }

        // Upload.
        InputStream in = null;
        try {

            // Use passive mode to pass firewalls.
            ftp.enterLocalPassiveMode();

            in = new BufferedInputStream(new FileInputStream(localFile));
            if(!ftp.storeFile(ftpFileName, in)) {
                throw new IOException("Can't upload file '"
                        + ftpFileName
                        + "' to FTP server. Check FTP permissions and path.");
            }

        } finally {
            try {
                in.close();
            } catch (IOException ex) {
            }
        }

        logger.DEBUG("FTP upload complete: file saved as '" + ftpFileName
                + "' on FTP.");

    }

    /**
     * Downloads the file from the FTP server.
     *
     * @param ftpFileName server file name (with absolute path)
     * @param localFile   local file to download into
     * @throws IOException on I/O errors
     */
    public void download(String ftpFileName, File localFile)
            throws IOException {

        String localFileName = localFile.getAbsolutePath();
        logger.DEBUG("FTP download: " + ftpFileName + " -> " + localFile);

        // File check.
        if(localFile.exists()) {
            logger.WARN("Local file '" + localFileName
                    + "' exists - overwriting...");
        } else {
            logger.INFO("Local file '" + localFileName
                    + "' doesn't exist - will be created...");
        }

        // Download.
        OutputStream out = null;
        try {

            // Use passive mode to pass firewalls.
            ftp.enterLocalPassiveMode();

            // Get file info.
            FTPFile[] fileInfoArray = ftp.listFiles(ftpFileName);
            if(fileInfoArray == null) {
                // Try to get '<file>'
                ftpFileName = "'" + ftpFileName + "'";
                fileInfoArray = ftp.listFiles(ftpFileName);
                if(fileInfoArray == null) {
                    throw new FileNotFoundException("File " + ftpFileName
                            + " was not found on FTP server.");
                }
            }

            // Check file size.
            FTPFile fileInfo = fileInfoArray[0];
            long size = fileInfo.getSize();
            if(size > Integer.MAX_VALUE) {
                throw new IOException("File " + ftpFileName + " is too large.");
            }

            // Download file.
            out = new BufferedOutputStream(new FileOutputStream(localFile));
            if(!ftp.retrieveFile(ftpFileName, out)) {
                throw new IOException("Error loading file " + ftpFileName +
                        " from FTP server. Check FTP permissions and path.");
            }
            out.flush();

        } finally {
            if(out != null) {
                try {
                    out.close();
                } catch (IOException ex) {
                }
            }
        }

        logger.DEBUG("FTP download complete: file saved as '" + localFileName
                + "' locally.");

    }

    /**
     * Removes the file from the FTP server.
     *
     * @param ftpFileName server file name (with absolute path)
     * @throws IOException on I/O errors
     */
    public void remove(String ftpFileName)
            throws IOException {

        if(!ftp.deleteFile(ftpFileName)) {
            throw new IOException(
                    "Can't remove file '" + ftpFileName + "' from FTP server.");
        }
    }

    /**
     * Lists the files in the given FTP directory.
     *
     * @param filePath absolute path on the server
     * @return files relative names list
     * @throws IOException on I/O errors
     */
    public List list(String filePath)
            throws IOException {

        List fileList = new ArrayList();
        FTPFile[] ftpFiles = ftp.listFiles(filePath);
        int size = (ftpFiles == null) ? 0:ftpFiles.length;
        for(int i = 0; i < size; i++) {
            FTPFile ftpFile = ftpFiles[i];
            if(ftpFile.isFile()) {
                fileList.add(ftpFile.getName());
            }
        }
        return fileList;
    }

    /**
     * Sends an FTP Server site specific command
     *
     * @param args site command arguments
     * @throws IOException on I/O errors
     */
    public void sendSiteCommand(String args)
            throws IOException {

        if(ftp.isConnected()) {
            try {
                ftp.sendSiteCommand(args);
            } catch (IOException ex) {
            }
        }
    }

    /**
     * Disconnects from the FTP server
     *
     * @throws IOException on I/O errors
     */
    public void disconnect()
            throws IOException {

        if(ftp.isConnected()) {
            try {
                ftp.logout();
                ftp.disconnect();
                is_connected = false;
            } catch (IOException ex) {
            }
        }
    }

    /**
     * Makes the full name of the file on the FTP server by joining its path
     * and the local file name.
     *
     * @param ftpPath   file path on the server
     * @param localFile local file
     * @return full name of the file on the FTP server
     */
    public String makeFTPFileName(String ftpPath, File localFile) {
        if(StringHelper.isEmpty(ftpPath)) {
            return localFile.getName();
        } else {
            String path = ftpPath.trim();
            if(path.charAt(path.length() - 1) != '/') {
                path = path + "/";
            }
            return path + localFile.getName();
        }
    }

    /**
     * Test coonection to ftp server
     *
     * @return true, if connected
     */
    public boolean isConnected() {
        return is_connected;
    }

    /**
     * Get current directory on ftp server
     *
     * @return current directory
     */
    public String getWorkingDirectory() {
        if(!is_connected) {
            return "";
        }
        try {
            return ftp.printWorkingDirectory();
        } catch (IOException e) {
        }
        return "";
    }

    /**
     * Set working directory on ftp server
     *
     * @param dir new working directory
     * @return true, if working directory changed
     */
    public boolean setWorkingDirectory(String dir) {
        if(!is_connected) {
            return false;
        }
        try {
            return ftp.changeWorkingDirectory(dir);
        } catch (IOException e) {
        }
        return false;
    }

    /**
     * Change working directory on ftp server to parent directory
     *
     * @return true, if working directory changed
     */
    public boolean setParentDirectory() {
        if(!is_connected) {
            return false;
        }
        try {
            return ftp.changeToParentDirectory();
        } catch (IOException e) {
        }
        return false;
    }

    /**
     * Get parent directory name on ftp server
     *
     * @return parent directory
     */
    public String getParentDirectory() {
        if(!is_connected) {
            return "";
        }
        String w = getWorkingDirectory();
        setParentDirectory();
        String p = getWorkingDirectory();
        setWorkingDirectory(w);
        return p;
    }

    /**
     * Get directory contents on ftp server
     *
     * @param filePath directory
     * @return list of FTPFileInfo structures
     * @throws IOException
     */
    public List listFiles(String filePath)
            throws IOException {
        List fileList = new ArrayList();
        FTPFile[] ftpFiles = ftp.listFiles(filePath);
        int size = (ftpFiles == null) ? 0:ftpFiles.length;
        for(int i = 0; i < size; i++) {
            FTPFile ftpFile = ftpFiles[i];
            FTPFileInfo fi = new FTPFileInfo();
            fi.setName(ftpFile.getName());
            fi.setSize(ftpFile.getSize());
            fi.setTimestamp(ftpFile.getTimestamp());
            fi.setType(ftpFile.isDirectory());
            fileList.add(fi);
        }

        return fileList;
    }

    /**
     * Get file from ftp server into given output stream
     *
     * @param ftpFileName file name on ftp server
     * @param out         OutputStream
     * @throws IOException
     */
    public void getFile(String ftpFileName, OutputStream out)
            throws IOException {

        try {

            // Get file info.
            FTPFile[] fileInfoArray = ftp.listFiles(ftpFileName);
            if(fileInfoArray == null) {
                throw new FileNotFoundException("File '" + ftpFileName
                        + "' was not found on FTP server.");
            }

            // Check file size.
            FTPFile fileInfo = fileInfoArray[0];
            long size = fileInfo.getSize();
            if(size > Integer.MAX_VALUE) {
                throw new IOException(
                        "File '" + ftpFileName + "' is too large.");
            }

            // Download file.
            if(!ftp.retrieveFile(ftpFileName, out)) {
                throw new IOException("Error loading file '"
                        + ftpFileName
                        + "' from FTP server. Check FTP permissions and path.");
            }
            out.flush();

        } finally {
            if(out != null) {
                try {
                    out.close();
                } catch (IOException ex) {
                }
            }
        }

    }

    /**
     * Put file on ftp server from given input stream
     *
     * @param ftpFileName file name on ftp server
     * @param in          InputStream
     * @throws IOException
     */
    public void putFile(String ftpFileName, InputStream in)
            throws IOException {
        try {
            // Use passive mode to pass firewalls.
            ftp.enterLocalPassiveMode();
            if(!ftp.storeFile(ftpFileName, in)) {
                throw new IOException("Can't upload file '"
                        + ftpFileName
                        + "' to FTP server. Check FTP permissions and path.");
            }
        } finally {
            try {
                in.close();
            } catch (IOException ex) {
            }
        }
    }

    // ============================================================ Inner class

    /**
     * File info class, containing information about file on ftp server. Has
     * only getters and setters methods
     */
    public static class FTPFileInfo
            implements Serializable {

        String name;
        long size;
        Calendar timestamp;
        boolean is_dir;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public long getSize() {
            return size;
        }

        public void setSize(long size) {
            this.size = size;
        }

        public Calendar getTimestamp() {
            return timestamp;
        }

        public void setTimestamp(Calendar timestamp) {
            this.timestamp = timestamp;
        }

        public boolean isDirectory() {
            return is_dir;
        }

        public void setType(boolean is_dir) {
            this.is_dir = is_dir;
        }
    }

}
