package net.gotev.uploadservice.ftp;

import android.content.Intent;

import net.gotev.uploadservice.Logger;
import net.gotev.uploadservice.ServerResponse;
import net.gotev.uploadservice.UploadFile;
import net.gotev.uploadservice.UploadService;
import net.gotev.uploadservice.UploadTask;

import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.commons.net.ftp.FTPSClient;
import org.apache.commons.net.io.CopyStreamEvent;
import org.apache.commons.net.io.CopyStreamListener;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;

/**
 * Implements the FTP upload logic.
 * @author Aleksandar Gotev
 */
public class FTPUploadTask extends UploadTask implements CopyStreamListener {

    private static final String LOG_TAG = FTPUploadTask.class.getSimpleName();

    // properties associated to each file
    protected static final String PARAM_REMOTE_PATH = "ftpRemotePath";
    protected static final String PARAM_PERMISSIONS = "ftpPermissions";

    private FTPUploadTaskParameters ftpParams = null;
    private FTPClient ftpClient = null;

    @Override
    protected void init(UploadService service, Intent intent) throws IOException {
        super.init(service, intent);
        this.ftpParams = intent.getParcelableExtra(FTPUploadTaskParameters.PARAM_FTP_TASK_PARAMETERS);
    }

    @Override
    protected void upload() throws Exception {
        try {
            if (ftpParams.useSSL) {
                String secureProtocol = ftpParams.secureSocketProtocol;

                if (secureProtocol == null || secureProtocol.isEmpty())
                    secureProtocol = FTPUploadTaskParameters.DEFAULT_SECURE_SOCKET_PROTOCOL;

                ftpClient = new FTPSClient(secureProtocol, ftpParams.implicitSecurity);

                Logger.debug(LOG_TAG, "Created FTP over SSL (FTPS) client with "
                        + secureProtocol + " protocol and "
                        + (ftpParams.implicitSecurity ? "implicit security" : "explicit security"));

            } else {
                ftpClient = new FTPClient();
            }

            ftpClient.setBufferSize(UploadService.BUFFER_SIZE);
            ftpClient.setCopyStreamListener(this);
            ftpClient.setDefaultTimeout(ftpParams.connectTimeout);
            ftpClient.setConnectTimeout(ftpParams.connectTimeout);
//            ftpClient.setAutodetectUTF8(true);

            Logger.debug(LOG_TAG, "Connect timeout set to " + ftpParams.connectTimeout + "ms");

            Logger.debug(LOG_TAG, "Connecting to " + params.serverUrl
                                  + ":" + ftpParams.port + " as " + ftpParams.username);
            ftpClient.connect(params.serverUrl, ftpParams.port);

            if (!FTPReply.isPositiveCompletion(ftpClient.getReplyCode())) {
                throw new Exception("Can't connect to " + params.serverUrl
                                    + ":" + ftpParams.port
                                    + ". The server response is: " + ftpClient.getReplyString());
            }

            if (!ftpClient.login(ftpParams.username, ftpParams.password)) {
                throw new Exception("Error while performing login on " + params.serverUrl
                                    + ":" + ftpParams.port
                                    + " with username: " + ftpParams.username
                                    + ". Check your credentials and try again.");
            }

            // to prevent the socket timeout on the control socket during file transfer,
            // set the control keep alive timeout to a half of the socket timeout
            int controlKeepAliveTimeout = ftpParams.socketTimeout / 2 / 1000;

            ftpClient.setSoTimeout(ftpParams.socketTimeout);
            ftpClient.setControlKeepAliveTimeout(controlKeepAliveTimeout);
            ftpClient.setControlKeepAliveReplyTimeout(controlKeepAliveTimeout * 1000);

            Logger.debug(LOG_TAG, "Socket timeout set to " + ftpParams.socketTimeout
                         + "ms. Enabled control keep alive every " + controlKeepAliveTimeout + "s");

//            ftpClient.enterLocalPassiveMode();
            ftpClient.enterLocalActiveMode();
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            ftpClient.setFileTransferMode(ftpParams.compressedFileTransfer ?
                                          FTP.COMPRESSED_TRANSFER_MODE : FTP.STREAM_TRANSFER_MODE);

            // this is needed to calculate the total bytes and the uploaded bytes, because if the
            // request fails, the upload method will be called again
            // (until max retries is reached) to retry the upload, so it's necessary to
            // know at which status we left, to be able to properly notify firther progress.
            calculateUploadedAndTotalBytes();

            String baseWorkingDir = ftpClient.printWorkingDirectory();
            Logger.debug(LOG_TAG, "FTP default working directory is: " + baseWorkingDir);

            Iterator<UploadFile> iterator = new ArrayList<>(params.files).iterator();
            while (iterator.hasNext()) {
                UploadFile file = iterator.next();

                if (!shouldContinue)
                    break;

                uploadFile(baseWorkingDir, file);
                addSuccessfullyUploadedFile(file);
                iterator.remove();
            }

            // Broadcast completion only if the user has not cancelled the operation.
            if (shouldContinue) {
                broadcastCompleted(new ServerResponse(UploadTask.TASK_COMPLETED_SUCCESSFULLY,
                                   UploadTask.EMPTY_RESPONSE, null));
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (ftpClient.isConnected()) {
                try {
                    Logger.debug(LOG_TAG, "Logout and disconnect from FTP server: "
                            + params.serverUrl + ":" + ftpParams.port);
                    ftpClient.logout();
                    ftpClient.disconnect();
                } catch (Exception exc) {
                    Logger.error(LOG_TAG, "Error while closing FTP connection to: "
                            + params.serverUrl + ":" + ftpParams.port, exc);
                }
            }
            ftpClient = null;
        }
    }

    /**
     * Calculates the total bytes of this upload task.
     * This the sum of all the lengths of the successfully uploaded files and also the pending
     * ones.
     */
    private void calculateUploadedAndTotalBytes() {
        uploadedBytes = 0;

        for (String filePath : getSuccessfullyUploadedFiles()) {
            uploadedBytes += new File(filePath).length();
        }

        totalBytes = uploadedBytes;

        for (UploadFile file : params.files) {
            totalBytes += file.length(service);
        }
    }

    private void uploadFile(String baseWorkingDir, UploadFile file) throws IOException {
        Logger.debug(LOG_TAG, "Starting FTP upload of: " + file.getName(service)
                              + " to: " + file.getProperty(PARAM_REMOTE_PATH));

        String remoteDestination = file.getProperty(PARAM_REMOTE_PATH);

        if (remoteDestination.startsWith(baseWorkingDir)) {
            remoteDestination = remoteDestination.substring(baseWorkingDir.length());
        }

        makeDirectories(remoteDestination, ftpParams.createdDirectoriesPermissions);

        InputStream localStream = file.getStream(service);
        try {
            String remoteFileName = getRemoteFileName(file);
            if (!ftpClient.storeFile(remoteFileName, localStream)) {
                throw new IOException("Error while uploading: " + file.getName(service)
                                      + " to: " + file.getProperty(PARAM_REMOTE_PATH));
            }

            setPermission(remoteFileName, file.getProperty(PARAM_PERMISSIONS));

        } finally {
            localStream.close();
        }

        // get back to base working directory
        if (!ftpClient.changeWorkingDirectory(baseWorkingDir)) {
            Logger.info(LOG_TAG, "Can't change working directory to: " + baseWorkingDir);
        }
    }

    private void setPermission(String remoteFileName, String permissions) {
        if (permissions == null || "".equals(permissions))
            return;

        // http://stackoverflow.com/questions/12741938/how-can-i-change-permissions-of-a-file-on-a-ftp-server-using-apache-commons-net
        try {
            if (ftpClient.sendSiteCommand("chmod " + permissions + " " + remoteFileName)) {
                Logger.error(LOG_TAG, "Error while setting permissions for: "
                        + remoteFileName + " to: " + permissions
                        + ". Check if your FTP user can set file permissions!");
            } else {
                Logger.debug(LOG_TAG, "Permissions for: " + remoteFileName + " set to: " + permissions);
            }
        } catch (IOException exc) {
            Logger.error(LOG_TAG, "Error while setting permissions for: "
                    + remoteFileName + " to: " + permissions
                    + ". Check if your FTP user can set file permissions!", exc);
        }
    }

    @Override
    public void bytesTransferred(CopyStreamEvent event) {
    }

    @Override
    public void bytesTransferred(long totalBytesTransferred, int bytesTransferred, long streamSize) {
        uploadedBytes += bytesTransferred;
        broadcastProgress(uploadedBytes, totalBytes);

        if (!shouldContinue) {
            try {
                ftpClient.disconnect();
            } catch (Exception exc) {
                Logger.error(LOG_TAG, "Failed to abort current file transfer", exc);
            }
        }
    }

    /**
     * Creates a nested directory structure on a FTP server and enters into it.
     * @param dirPath Path of the directory, i.e /projects/java/ftp/demo
     * @param permissions UNIX permissions to apply to created directories. If null, the FTP
     *                    server defaults will be applied, because no UNIX permissions will be
     *                    explicitly set
     * @throws IOException if any error occurred during client-server communication
     */
    private void makeDirectories(String dirPath, String permissions) throws IOException {
        if (!dirPath.contains("/")) return;

        String[] pathElements = dirPath.split("/");

        if (pathElements.length == 1) return;

        // if the string ends with / it means that the dir path contains only directories,
        // otherwise if it does not contain /, the last element of the path is the file name,
        // so it must be ignored when creating the directory structure
        int lastElement = dirPath.endsWith("/") ? pathElements.length : pathElements.length - 1;

        for (int i = 0; i < lastElement; i++) {
            String singleDir = pathElements[i];
            if (singleDir.isEmpty()) continue;

            if (!ftpClient.changeWorkingDirectory(singleDir)) {
                if (ftpClient.makeDirectory(singleDir)) {
                    Logger.debug(LOG_TAG, "Created remote directory: " + singleDir);
                    if (permissions != null) {
                        setPermission(singleDir, permissions);
                    }
                    ftpClient.changeWorkingDirectory(singleDir);
                } else {
                    throw new IOException("Unable to create remote directory: " + singleDir);
                }
            }
        }
    }

    /**
     * Checks if the remote file path contains also the remote file name. If it's not specified,
     * the name of the local file will be used.
     * @param file file to upload
     * @return remote file name
     */
    private String getRemoteFileName(UploadFile file) {

        // if the remote path ends with /
        // it means that the remote path specifies only the directory structure, so
        // get the remote file name from the local file
        if (file.getProperty(PARAM_REMOTE_PATH).endsWith("/")) {
            return file.getName(service);
        }

        // if the remote path contains /, but it's not the last character
        // it means that I have something like: /path/to/myfilename
        // so the remote file name is the last path element (myfilename in this example)
        if (file.getProperty(PARAM_REMOTE_PATH).contains("/")) {
            String[] tmp = file.getProperty(PARAM_REMOTE_PATH).split("/");
            return tmp[tmp.length - 1];
        }

        // if the remote path does not contain /, it means that it specifies only
        // the remote file name
        return file.getProperty(PARAM_REMOTE_PATH);
    }
}
