/**
 * Copyright (c) 2007-2012 EBM WebSourcing, 2012-2018 Linagora
 * 
 * This program/library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or (at your
 * option) any later version.
 * 
 * This program/library 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 Lesser General Public License
 * for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program/library; If not, see http://www.gnu.org/licenses/
 * for the GNU Lesser General Public License version 2.1.
 */
package org.ow2.petals.bc.ftp.connection;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.activation.DataHandler;
import javax.activation.FileTypeMap;
import javax.mail.util.ByteArrayDataSource;

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 org.ow2.petals.component.framework.util.FileNamePatternUtil;
import org.ow2.petals.component.framework.util.XMLUtil;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import com.ebmwebsourcing.easycommons.lang.StringHelper;

/**
 * This class handles a connection to the FTP server with the specified configuration parameter.
 * @author Mathieu CARROLLE - EBM WebSourcing
 */
public class WrappedFTPClient extends FTPClient {

    private final FTPConnectionInfo connectionInfo;

    private final transient Logger logger;

    public WrappedFTPClient(final FTPConnectionInfo connectionInfo, final Logger logger) {
        super();
        this.connectionInfo = connectionInfo;
        this.logger = logger;
    }

    /**
     * This method sets :
     * <ul>
     * <li>the working directory</li>
     * <li>the data connection mode (Active or Passive)</li>
     * <li>the type of transfer (ASCII or Binary)</li>
     * </ul>
     * 
     * @throws IOException
     */
    public void configureConnection() throws IOException {
        // Working directory
        boolean cd = true;
        if (!StringHelper.isNullOrEmpty(connectionInfo.getDirectory())) {
            cd = this.changeWorkingDirectory(connectionInfo.getDirectory());
        }
        if (cd == false) {
            throw new IOException("Failed to change directory. Target directory : ["
                    + connectionInfo.getDirectory() + "]");
        }
        logger.fine("FTP server working directory is : " + this.printWorkingDirectory());
        // connection mode
        if (connectionInfo.isPassiveMode()) {
            this.enterLocalPassiveMode();
            logger.fine("enter Passive mode");
        }
        // transfer mode
        if (connectionInfo.isAsciiTransferType()) {
            this.setFileType(FTP.ASCII_FILE_TYPE);
            logger.fine("ascii transfer mode enable");
        } else if (connectionInfo.isBinaryTransferType()) {
            this.setFileType(FTP.BINARY_FILE_TYPE);
            logger.fine("binary transfer mode enable");
        }
    }

    /**
     * Deleted the specified remote file.
     * 
     * @param remoteFile
     *            Remote file to delete
     * @throws IOException
     */
    public void del(final String remoteFile) throws IOException {
        int numberOfTries = 0;
        int maxTries = connectionInfo.getAttempt();
        boolean deleted = false;
        while (!deleted) {
            numberOfTries++;
            if (numberOfTries > maxTries) {
                throw new IOException("Error occurred while deleting '" + remoteFile
                        + "' file from the FTP server");
            }

            try {
                if (this.logger.isLoggable(Level.FINE)) {
                    logger.fine("start deleting '" + remoteFile + "' file from the FTP server");
                }
                deleted = this.deleteFile(remoteFile);
            } catch (IOException e) {
                if (numberOfTries == maxTries) {
                    throw e;
                } else {
                    this.logger.warning("An attempt to delete the file '" + remoteFile
                            + "' failed : " + e.getMessage());
                }
                try {
                    Thread.sleep(connectionInfo.getDelay());
                } catch (InterruptedException ex1) {
                    throw new IOException(ex1);
                }
            }
        }
    }

    /**
     * Get a file content from FTP server.
     * 
     * @param filePattern
     *            the remote file
     * 
     * @return The file content as XML {@link Document}
     * @throws IOException
     */
    public Document get(final String filePattern) throws IOException {
        Document doc = null;
        int numberOfTries = 0;
        final int maxTries = connectionInfo.getAttempt();

        while (doc == null) {
            numberOfTries++;

            if (numberOfTries > maxTries) {
                throw new IOException("Error occurred while getting '" + filePattern
                        + "' file from the FTP server");
            }

            try {
                logger.fine("start getting '" + filePattern + "' file from the FTP server");
                final FTPFile[] files = this.listFiles(filePattern);
                if (files != null && files.length > 0) {
                    doc = getAsDocument(files[0].getName());
                } else {
                    throw new IOException("Can not find the specified resource");
                }
            } catch (IOException e) {
                if (numberOfTries == maxTries) {
                    throw e;
                } else {
                    this.logger.warning("An attempt to get the file '" + filePattern
                            + "' failed : " + e.getMessage());
                }
                try {
                    Thread.sleep(connectionInfo.getDelay());
                } catch (InterruptedException ex1) {
                    throw new IOException(ex1);
                }
            }
        }
        return doc;
    }

    public FTPConnectionInfo getConnectionInfo() {
        return connectionInfo;
    }

    /**
     * Get a file content as attachment from FTP server.
     * 
     * @param filePatternWildCard
     *            the remote file
     * 
     * @return String
     * @throws IOException
     */
    public DataHandler getFileAsAttachment(final String filePatternWildCard) throws IOException {
        DataHandler data = null;
        int numberOfTries = 0;
        final int maxTries = connectionInfo.getAttempt();
        while (data == null) {
            numberOfTries++;

            if (numberOfTries > maxTries) {
                throw new IOException("Error occurred while getting '" + filePatternWildCard
                        + "' file from the FTP server");
            }

            try {
                logger.fine("start getting '" + filePatternWildCard
                        + "' file from the FTP server");

                FTPFile[] files = this.listFiles(filePatternWildCard);
                if (files != null && files.length > 0) {
                    data = getAsDataHandler(files[0]);
                } else {
                    throw new IOException("Can not find the specified resource");
                }
            } catch (IOException e) {
                if (numberOfTries == maxTries) {
                    throw e;
                } else {
                    this.logger.warning("An attempt to get the file '" + filePatternWildCard
                            + "' failed : " + e.getMessage());
                }
                try {
                    Thread.sleep(connectionInfo.getDelay());
                } catch (InterruptedException e1) {
                    throw new IOException(e1);
                }
            }
        }
        return data;
    }

    /**
     * Connect and log the FTP client with the current configuration
     * 
     * @throws IOException
     */
    public void connectAndLog() throws IOException {
        logger.fine("create FTP connection to " + connectionInfo.getServer());
        // set encoding before connect to the fTP server
        this.setControlEncoding(connectionInfo.getEncoding());
        this.connect(connectionInfo.getServer(), connectionInfo.getPort());
        int reply = this.getReplyCode();
        if (!FTPReply.isPositiveCompletion(reply)) {
            this.disconnect();
            throw new IOException("connection to FTP server failed.");
        }
        // Login
        boolean logged = this.login(connectionInfo.getUser(), connectionInfo.getPassword());
        if (!logged) {
            throw new IOException("authentication failed.");
        }
        logger.fine(connectionInfo.toString());
    }

    /**
     * List files from FTP server
     * 
     * @return
     * @throws IOException
     */
    public List<String> listFolderContent() throws IOException {
        List<String> fileList = new ArrayList<String>();
        boolean listAllFiles = false;
        int numberOfTries = 0;
        int maxTries = connectionInfo.getAttempt();
        while (!listAllFiles) {
            try {
                logger.fine("start listing a directory from the FTP server");
                for (FTPFile file : this.listFiles()) {
                    if (file.isFile()) {
                        fileList.add(file.getName());
                    }
                }
                listAllFiles = true;
            } catch (IOException e) {
                numberOfTries++;
                if (numberOfTries == maxTries) {
                    throw e;
                } else {
                    this.logger
                            .warning("An attempt to list a directory failed : " + e.getMessage());
                }
                try {
                    Thread.sleep(connectionInfo.getDelay());
                } catch (InterruptedException ex1) {
                    throw new IOException(ex1);
                }
            }
        }
        return fileList;
    }

    public void mDel(List<String> remoteFiles) throws IOException {
        int numberOfTries = 0;
        int maxTries = connectionInfo.getAttempt();
        boolean deletedAllFile = false;
        while (!deletedAllFile) {
            try {
                if (this.logger.isLoggable(Level.FINE)) {
                    logger.fine("start deleting '" + remoteFiles.toString()
                            + "' file from the FTP server");
                }

                for (String pathname : remoteFiles) {
                    this.deleteFile(pathname);
                }
                deletedAllFile = true;
            } catch (IOException e) {
                if (numberOfTries == maxTries) {
                    throw e;
                } else {
                    this.logger.warning("An attempt to delete the file '" + remoteFiles
                            + "' failed : " + e.getMessage());
                }
                try {
                    Thread.sleep(connectionInfo.getDelay());
                } catch (InterruptedException ex1) {
                    throw new IOException(ex1);
                }
            }
        }
    }

    /**
     * This method will only get files, it does not include subpath
     * 
     * @param filePatternWildCardList
     * @return
     * @throws IOException
     */
    public Map<String, DataHandler> mGet(final List<String> filePatternWildCardList)
            throws IOException {
        boolean getAllFiles = false;
        int numberOfTries = 0;
        int maxTries = connectionInfo.getAttempt();
        Map<String, DataHandler> datahandlers = new HashMap<>();

        // Construct the fileFilters to match files read from the FTP directory
        final List<FilenameFilter> fileNameFilters = new LinkedList<>();
        for (final String string : filePatternWildCardList) {
            fileNameFilters.add(FileNamePatternUtil.buildFileNameFilterFromWildCard(
                    string));
        }
        final FilenameFilter filter = FileNamePatternUtil.buildFileNameFilterFromFilters(fileNameFilters);

        while (!getAllFiles) {

            try {
                if (this.logger.isLoggable(Level.FINE)) {
                    logger.fine("start getting files from the FTP server");
                }
                for (FTPFile file : this.listFiles()) {
                    if (file.isFile() && filter.accept(null, file.getName())) {
                        datahandlers.put(file.getName(), getAsDataHandler(file));
                    }
                }
                getAllFiles = true;
            } catch (IOException e) {
                numberOfTries++;
                if (numberOfTries >= maxTries) {
                    throw e;
                } else {
                    this.logger.warning("An attempt to get a file failed : " + e.getMessage());
                }
                try {
                    Thread.sleep(connectionInfo.getDelay());
                } catch (InterruptedException ex1) {
                    throw new IOException(ex1);
                }
            }
        }
        return datahandlers;
    }

    /**
     * Put files on the FTP server.
     * 
     * @param attachments
     * @throws IOException
     */
    public void mPut(Map<String, DataHandler> attachments) throws IOException {
        int numberOfTries = 0;
        int maxTries = connectionInfo.getAttempt();
        boolean transmit;
        boolean isFileExist;
        for (Entry<String, DataHandler> entry : attachments.entrySet()) {
            transmit = false;
            isFileExist = false;
            while (!transmit) {

                if (numberOfTries == maxTries) {
                    throw new IOException("Error occurred while storing '" + entry.getKey()
                            + "' file on the FTP server");
                }
                try {
                    logger.fine("start sending '" + entry.getKey() + "' file on the FTP server");
                    isFileExist = this.fileExistOnRemoteDirectory(entry.getKey());
                    if (!isFileExist || this.connectionInfo.isOverwrite()) {
                        InputStream stream = entry.getValue().getInputStream();
                        transmit = this.storeFile(entry.getKey(), stream);
                        stream.close();
                    } else {
                        // hack to stop retry policy
                        numberOfTries = maxTries;
                        throw new IOException("File [" + entry.getKey() + "] already exists");
                    }
                    if (transmit) {
                        logger.fine("'" + entry.getKey() + "' sent on the FTP server");
                    } else {
                        numberOfTries++;
                    }
                } catch (IOException e) {
                    numberOfTries++;
                    if (numberOfTries >= maxTries) {
                        throw e;
                    } else {
                        this.logger.log(Level.WARNING, "An attempt to put the file '" + entry.getKey() + "' failed", e);
                    }
                    try {
                        Thread.sleep(connectionInfo.getDelay());
                    } catch (InterruptedException ex1) {
                        throw new IOException(ex1);
                    }
                }
            }
        }
    }

    /**
     * Put the file on the FTP server.
     * 
     * @param fileName
     * @param content
     * @throws IOException
     */
    public void putString(final String fileName, String content) throws IOException {
        boolean transmit = false;
        boolean fileExist = false;
        int numberOfTries = 0;
        int maxTries = connectionInfo.getAttempt();
        while (!transmit) {
            numberOfTries++;

            if (numberOfTries > maxTries) {
                throw new IOException("Error occurred while storing '" + fileName
                        + "' file on the FTP server");
            }
            try {
                logger.fine("start sending '" + fileName + "' file on the FTP server");
                fileExist = this.fileExistOnRemoteDirectory(fileName);
                if (!fileExist || this.connectionInfo.isOverwrite()) {
                    OutputStream ops = this.storeFileStream(fileName);
                    ops.write(content.getBytes());
                    ops.flush();
                    ops.close();
                    transmit = this.completePendingCommand();
                } else {
                    // hack to stop retry policy
                    numberOfTries = maxTries;
                    throw new IOException("File [" + fileName + "] already exists");
                }
            } catch (IOException e) {
                if (numberOfTries >= maxTries) {
                    throw e;
                } else {
                    this.logger.warning("An attempt to put the file '" + fileName + "' failed : "
                            + e.getMessage());
                }
                try {
                    Thread.sleep(connectionInfo.getDelay());
                } catch (InterruptedException ex1) {
                    throw new IOException(ex1);
                }
            }
        }
        logger.fine("'" + fileName + "' sent on the FTP server");
    }

    /***
     * Search if the file exist on the remote directory
     * 
     * @param srcFileName
     *            Name of the file
     * @param client
     * @return true if the file exist on the remote directory (case-sensitive)
     * @throws IOException
     */
    private boolean fileExistOnRemoteDirectory(String srcFileName) throws IOException {
        boolean fileExist = false;
        String[] remoteFileNames = this.listNames();
        for (String remoteFileName : remoteFileNames) {
            if (remoteFileName.equals(srcFileName)) {
                fileExist = true;
                break;
            }
        }
        return fileExist;
    }

    /**
     * The ftp-client MUST be connected. This method DOES NOT close the
     * ftp-client
     * 
     * @param remoteFile
     * @param client
     *            a ftpclient, MUST be CONNECTED
     * @return the retrieved file, as a datahandler
     * @throws IOException
     *             the file is not found or FTP access problem
     */
    private DataHandler getAsDataHandler(FTPFile remoteFile) throws IOException {
        logger.fine("start receiving file " + remoteFile.getName() + " from FTP server");
        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(
                (int) remoteFile.getSize());
        try {
            this.retrieveFile(remoteFile.getName(), outputStream);
            outputStream.flush();
        } catch (IOException e) {
            throw e;
        } finally {
            outputStream.close();
        }
        logger.fine(remoteFile.getName() + " file received from FTP server");
        final ByteArrayDataSource datasource = new ByteArrayDataSource(outputStream.toByteArray(),
                FileTypeMap.getDefaultFileTypeMap().getContentType(remoteFile.getName()));
        datasource.setName(remoteFile.getName());
        final DataHandler result = new DataHandler(datasource);
        logger.fine(remoteFile.getName() + " set as DataHandler");
        return result;
    }

    /**
     * Extract the content of the specified XML file and returned it as XML
     * document
     * 
     * @param remoteFile
     * @return the retrieved file, as a Document
     * @throws IOException
     *             the file is not found or FTP access problem
     */
    private Document getAsDocument(String remoteFileName) throws IOException {
        final Document doc;
        if (this.logger.isLoggable(Level.FINE)) {
            logger.fine("start receiving file " + remoteFileName + " from FTP server");
        }
        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        this.retrieveFile(remoteFileName, bos);
        bos.flush();
        final ByteArrayInputStream is = new ByteArrayInputStream(bos.toByteArray());
        try {
            doc = XMLUtil.loadDocument(is);
        } catch (SAXException e) {
            throw new IOException("processed file '" + remoteFileName
                    + "' is not a valid xml file : " + e.getMessage());
        } finally {
            bos.close();
            is.close();
        }
        if (this.logger.isLoggable(Level.FINE)) {
            logger.fine(remoteFileName + " file received from FTP server and set as document");
        }
        return doc;
    }
}
