package com.jscape.filetransfer;

import com.jscape.util.Clock;
import com.jscape.util.Time;

import java.io.File;

import static com.jscape.util.Parameters.assertGreater;
import static com.jscape.util.Parameters.assertNotNull;

/**
 * @author Alex Usun
 */
public class DownloadFileOperation
        implements FileTransferOperation {

    private final FileTransferOperation postConnectOperation;
    private final RemoteDirectory remoteDirectory;
    private final String remoteFile;
    private final File localFile;
    private final TransferStrategy transferStrategy;
    private final int maxAttempts;
    private final Time attemptPeriod;
    private final boolean deleteRemoteFile;
    private final Listener listener;
    private FileTransfer transfer;
    private boolean disconnectOnCompleted;
    private String currentRemoteDirectory;
    private int attempts;
    private boolean success;
    private Exception error;
    private volatile boolean cancelled;

    public DownloadFileOperation(FileTransferOperation postConnectOperation,
                                 RemoteDirectory remoteDirectory,
                                 String remoteFile,
                                 File localFile,
                                 TransferStrategy transferStrategy,
                                 int maxAttempts,
                                 Time attemptPeriod,
                                 boolean deleteRemoteFile,
                                 Listener listener) {
        assertNotNull(postConnectOperation);
        this.postConnectOperation = postConnectOperation;

        assertNotNull(remoteDirectory);
        this.remoteDirectory = remoteDirectory;

        this.remoteFile = remoteFile != null && !remoteFile.isEmpty() ? remoteFile : localFile.getName();

        assertNotNull(localFile);
        this.localFile = localFile;

        assertNotNull(transferStrategy);
        this.transferStrategy = transferStrategy;

        assertGreater(maxAttempts, 0, "Bad max attempts value.");
        this.maxAttempts = maxAttempts;

        assertNotNull(attemptPeriod);
        this.attemptPeriod = attemptPeriod;

        this.deleteRemoteFile = deleteRemoteFile;

        assertNotNull(listener);
        this.listener = listener;
    }

    @Override
    public DownloadFileOperation applyTo(FileTransfer transfer)
            throws FileTransferException {
        assertNotNull(transfer);
        this.transfer = transfer;

        try {
            while (shouldAttempt()) {
                nextAttempt();
            }

            assertNotCancelled();
            assertNoErrors();

            return this;
        } catch (Exception e) {
            throw FileTransferException.wrap(e);
        } finally {
            cleanup();
            this.listener.onOperationCompleted(this);
        }
    }

    public void cancel() {
        this.cancelled = true;

        FileTransfer transfer = this.transfer;

        if (transfer != null) {
            transfer.interrupt();
        }
    }

    @Override
    public String toString() {
        return "DownloadFileOperation {"
                + "postConnectOperation=" + this.postConnectOperation
                + ", remoteDirectory=" + this.remoteDirectory
                + ", remoteFile='" + this.remoteFile + '\''
                + ", localFile=" + this.localFile
                + ", transferStrategy=" + this.transferStrategy
                + ", maxAttempts=" + this.maxAttempts
                + ", attemptPeriod=" + this.attemptPeriod
                + ", deleteRemoteFile=" + this.deleteRemoteFile
                + ", attempts=" + this.attempts
                + ", success=" + this.success
                + ", error=" + this.error
                + ", cancelled=" + this.cancelled
                + '}';
    }

    private boolean shouldAttempt() {
        return !this.success && this.attempts < this.maxAttempts && !this.cancelled;
    }

    private void nextAttempt() {
        initAttempt();
        awaitAttemptPeriod();

        try {
            initTransfer();
            transferFile();
            deleteRemoteFile();
            restoreCurrentDir();
            attemptSucceeded();
        } catch (Exception e) {
            attemptFailed(e);
            disconnect();
        }
    }

    private void assertNotCancelled()
            throws TransferOperationCancelledException {
        if (this.cancelled) {
            throw new TransferOperationCancelledException();
        }
    }

    private void assertNoErrors()
            throws Exception {
        if (this.error != null) {
            throw this.error;
        }
    }

    private void initAttempt() {
        this.success = false;
        this.error = null;
    }

    private void awaitAttemptPeriod() {
        if (this.attempts > 0) {
            Clock.await(this.attemptPeriod.toMillis());
        }
    }

    private void initTransfer()
            throws Exception {
        if (!this.transfer.isConnected()) {
            this.disconnectOnCompleted = true;
            this.transfer.connect();
            this.postConnectOperation.applyTo(this.transfer);
        }

        this.currentRemoteDirectory = this.transfer.getDir();
        this.remoteDirectory.applyTo(this.transfer);

        File localDir = this.localFile.getParentFile();
        this.transfer.setLocalDir(localDir != null ? localDir : new File("."));
    }

    private void transferFile()
            throws Exception {
        this.transferStrategy.apply(this);
    }

    private void deleteRemoteFile() {
        if (!this.deleteRemoteFile) {
            return;
        }

        try {
            this.transfer.deleteFile(this.remoteFile);
        } catch (Exception e) {
            // ignore
        }
    }

    private void restoreCurrentDir() {
        try {
            this.transfer.setDir(this.currentRemoteDirectory);
        } catch (Exception e) {
            // ignore
        }
    }

    private void attemptSucceeded() {
        this.success = true;
    }

    private void attemptFailed(Exception e) {
        this.error = e;
        ++this.attempts;
    }

    private void cleanup() {
        if (this.disconnectOnCompleted) {
            disconnect();
        }
    }

    private void disconnect() {
        try {
            this.transfer.disconnect();
        } catch (Exception e) {
            // ignore
        }
    }

    public interface Listener {

        void onOperationCompleted(DownloadFileOperation operation);

    }

    public static class NullListener
            implements Listener {

        public void onOperationCompleted(DownloadFileOperation operation) {
            // empty
        }
    }

    public enum TransferStrategy {

        FROM_SCRATCH() {
            @Override
            public void apply(DownloadFileOperation operation)
                    throws Exception {
                operation.transfer.download(operation.localFile.getName(), operation.remoteFile);
            }
        },
        ALWAYS_RESUME() {
            @Override
            public void apply(DownloadFileOperation operation)
                    throws Exception {
                try {
                    long offset = operation.localFile.length();
                    operation.transfer.resumeDownload(operation.remoteFile, offset);
                } catch (Exception e) {
                    FROM_SCRATCH.apply(operation);
                }
            }
        },
        RESUME_AFTER_FIRST_ATTEMPT() {
            @Override
            public void apply(DownloadFileOperation operation)
                    throws Exception {
                if (operation.attempts > 0) {
                    ALWAYS_RESUME.apply(operation);
                } else {
                    FROM_SCRATCH.apply(operation);
                }
            }
        };

        public abstract void apply(DownloadFileOperation operation)
                throws Exception;
    }

}
