package com.jscape.filetransfer;

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

import java.io.File;
import java.nio.file.Files;

import static com.jscape.util.Parameters.*;

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

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

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

        assertNotNull(remoteDirectory);
        this.remoteDirectory = remoteDirectory;

        assertTrue(localFile.isFile(), "Specified pathname is not a file.");
        this.localFile = localFile;

        String remoteFileExtension = StringTools.isValidString(tempFileExtension) ? tempFileExtension.startsWith(".") ? tempFileExtension : "." + tempFileExtension : "";
        this.remoteFile = localFile.getName() + remoteFileExtension;

        assertNotNull(transferStrategy);
        this.transferStrategy = transferStrategy;

        this.renameRemoteFileRequired = remoteFileExtension.length() > 0;

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

        assertNotNull(attemptPeriod);
        this.attemptPeriod = attemptPeriod;

        this.deleteLocalFileRequired = deleteLocalFileRequired;

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

    @Override
    public UploadFileOperation 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 "UploadFileOperation {"
                + "postConnectOperation=" + this.postConnectOperation
                + ", localFile=" + this.localFile
                + ", remoteDirectory=" + this.remoteDirectory
                + ", remoteFile='" + this.remoteFile + '\''
                + ", transferStrategy=" + this.transferStrategy
                + ", renameRemoteFileRequired=" + this.renameRemoteFileRequired
                + ", maxAttempts=" + this.maxAttempts
                + ", attemptPeriod=" + this.attemptPeriod
                + ", deleteLocalFileRequired=" + this.deleteLocalFileRequired
                + ", attempts=" + this.attempts
                + ", succeeded=" + this.succeeded
                + ", error=" + this.error
                + ", cancelled=" + this.cancelled
                + '}';
    }

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

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

        try {
            initTransfer();
            transferFile();
            completeTransfer();
            deleteLocalFile();
            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.succeeded = 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);
    }

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

    private void completeTransfer()
            throws Exception {
        if (this.renameRemoteFileRequired) {
            this.transfer.renameFile(this.remoteFile, this.localFile.getName());
        }
    }

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

        try {
            Files.delete(this.localFile.toPath());
        } catch (Exception e) {
            // ignore
        }
    }

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

    private void attemptSucceeded() {
        this.succeeded = 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(UploadFileOperation operation);

    }

    public static class NullListener
            implements Listener {

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

    public enum TransferStrategy {

        FROM_SCRATCH() {
            @Override
            public void apply(UploadFileOperation operation)
                    throws Exception {
                operation.transfer.upload(operation.localFile, operation.remoteFile);
            }
        },
        ALWAYS_RESUME() {
            @Override
            public void apply(UploadFileOperation operation)
                    throws Exception {
                try {
                    long offset = operation.transfer.getFilesize(operation.remoteFile);
                    operation.transfer.resumeUpload(operation.localFile, offset);
                } catch (Exception e) {
                    FROM_SCRATCH.apply(operation);
                }
            }
        },
        RESUME_AFTER_FIRST_ATTEMPT() {
            @Override
            public void apply(UploadFileOperation operation)
                    throws Exception {
                if (operation.attempts > 0) {
                    ALWAYS_RESUME.apply(operation);
                } else {
                    FROM_SCRATCH.apply(operation);
                }
            }
        };

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


}
