package com.jscape.filetransfer;

import com.jscape.util.Attemptable;
import com.jscape.util.Time;
import com.jscape.util.collections.Tree;

import java.io.File;
import java.util.*;
import java.util.concurrent.*;

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

/**
 * @author Alex Usun
 */
public class UploadDirectoryOperation
        implements FileTransferOperation,
                   UploadFileOperation.Listener {

    private final FileTransferOperation postConnectOperation;
    private final File localDirectory;
    private final String tempFileExtension;
    private final int maxAttempts;
    private final Time attemptPeriod;
    private final int threadCount;
    private FileTransfer transfer;
    private boolean disconnectOnCompleted;
    private Map<File, UploadFileOperation> fileOperations;
    private ExecutorService executorService;
    private BlockingQueue<FileTransfer> transfers;
    private Tree<DirectoryService.Directory<File>> localDirectoryTree;
    private CountDownLatch doneSignal;
    private volatile boolean cancelled;

    public UploadDirectoryOperation(FileTransferOperation postConnectOperation,
                                    File localDirectory,
                                    String tempFileExtension,
                                    int maxAttempts,
                                    Time attemptPeriod,
                                    Integer threadCount) {
        assertNotNull(postConnectOperation);
        this.postConnectOperation = postConnectOperation;

        assertTrue(localDirectory.isDirectory(), "Specified pathname is not a directory.");
        this.localDirectory = localDirectory;

        this.tempFileExtension = tempFileExtension;

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

        assertNotNull(attemptPeriod);
        this.attemptPeriod = attemptPeriod;

        assertTrue(threadCount == null || threadCount >= 0, "Bad thread count value.");
        this.threadCount = threadCount != null ? threadCount : 0;
    }

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

        this.cancelled = false;

        try {
            initLocalDirectoryTree();
            createRemoteDirectoryTree();
            initFileOperations();
            initDoneSignal();

            if (useThreads()) {
                startFileOperations();
            } else {
                runFileOperations();
            }

            awaitOperationsDone();
            assertNotCancelled();

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

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

        for (UploadFileOperation operation : this.fileOperations.values()) {
            operation.cancel();
        }
    }

    public void cancel(File file) {
        UploadFileOperation operation = findOperationFor(file);

        if (operation != null) {
            operation.cancel();
        }
    }

    public int fileCount() {
        return this.fileOperations.size();
    }

    public void onOperationCompleted(UploadFileOperation operation) {
        this.doneSignal.countDown();
    }

    @Override
    public String toString() {
        return "UploadDirectoryOperation {"
                + "postConnectOperation=" + this.postConnectOperation
                + ", localDirectory=" + this.localDirectory
                + ", tempFileExtension='" + this.tempFileExtension + '\''
                + ", maxAttempts=" + this.maxAttempts
                + ", attemptPeriod=" + this.attemptPeriod
                + ", threadCount=" + this.threadCount
                + ", localDirectoryTree=" + this.localDirectoryTree
                + ", cancelled=" + this.cancelled
                + '}';
    }

    private void initLocalDirectoryTree() {
        this.localDirectoryTree = DirectoryService.asTree(this.localDirectory);
    }

    private void createRemoteDirectoryTree()
            throws Exception {
        Attemptable.run(new Callable<Object>() {
            @Override
            public Object call()
                    throws Exception {
                try {
                    initTransfer();
                    createRemoteDirectoryTree(localDirectoryTree);
                    return null;
                } catch (Exception e) {
                    disconnect();
                    throw e;
                }
            }
        }, this.attemptPeriod.toMillis(), this.maxAttempts);
    }

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

    private void initFileOperations()
            throws Exception {
        this.fileOperations = new HashMap<>();
        List<String> remoteDirectoryPath = currentRemoteDirectoryPath();
        initFileOperations(this.localDirectoryTree, remoteDirectoryPath);
    }

    private void initDoneSignal() {
        this.doneSignal = new CountDownLatch(this.fileOperations.size());
    }

    private boolean useThreads() {
        return this.threadCount > 1;
    }

    private void startFileOperations()
            throws Exception {
        int connectionCount = Math.min(this.threadCount, this.fileOperations.size());
        this.executorService = Executors.newFixedThreadPool(connectionCount);
        initTransfers(connectionCount);
    
        for (FileTransferOperation operation : this.fileOperations.values()) {
            this.executorService.submit(new CallableQueuedTransferOperation(operation, this.transfers));
        }
    }
    
    private void initTransfers(int connectionCount)
            throws Exception {
        this.transfers = new ArrayBlockingQueue<>(connectionCount);
        this.transfers.offer(this.transfer);
        
        try {
            for (int i = 1; i < connectionCount; ++i) {
                FileTransfer transfer = this.transfer.copy();
                connect(transfer);
                this.transfers.offer(transfer);
            }
        } catch (Exception e) {
            closeTransfers();
            throw e;
        }
    }
    
    private void closeTransfers() {
        if (this.transfers == null) {
            return;
        }
        
        FileTransfer transfer;
        while ((transfer = this.transfers.poll()) != null) {
            transfer.close();
        }
    }
    
    private void connect(FileTransfer transfer)
            throws FileTransferException {
        transfer.connect();
        this.postConnectOperation.applyTo(transfer);
    }

    private void awaitOperationsDone() {
        try {
            this.doneSignal.await();
        } catch (InterruptedException e) {
            // todo: process error
        } finally {
            closeTransfers();
        }
    }

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

    private void cleanup() {
        if (this.executorService != null) {
            this.executorService.shutdownNow();
            this.executorService = null;
        }

        this.localDirectoryTree = null;
        this.doneSignal = null;

        if (this.disconnectOnCompleted) {
            disconnect();
        }
    }

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

    private void createRemoteDirectoryTree(Tree<DirectoryService.Directory<File>> localTree)
            throws Exception {
        String directoryName = localTree.value().path.getName();

        if (!this.transfer.exists(directoryName)) {
            this.transfer.makeDir(directoryName);
        }

        this.transfer.setDir(directoryName);

        for (Tree<DirectoryService.Directory<File>> leaf : localTree.leafs()) {
            createRemoteDirectoryTree(leaf);
        }

        this.transfer.setDirUp();
    }

    private List<String> currentRemoteDirectoryPath()
            throws Exception {
        LinkedList<String> path = new LinkedList<>();
        path.add(this.transfer.getDir());

        return path;
    }

    private void initFileOperations(Tree<DirectoryService.Directory<File>> localTree, List<String> remoteDirectoryPath) {
        remoteDirectoryPath.add(localTree.value().path.getName());

        initFileOperations(localTree.value().files, new RemoteDirectory.PathElementsDirectory(remoteDirectoryPath));

        for (Tree<DirectoryService.Directory<File>> leaf : localTree.leafs()) {
            initFileOperations(leaf, new LinkedList<>(remoteDirectoryPath));
        }
    }

    private void initFileOperations(Collection<File> files, RemoteDirectory remoteDirectory) {
        for (File file : files) {
            UploadFileOperation operation = operationFor(file, remoteDirectory);
            this.fileOperations.put(file, operation);
        }
    }

    private UploadFileOperation findOperationFor(File file) {
        return this.fileOperations.get(file);
    }

    private UploadFileOperation operationFor(File file, RemoteDirectory remoteDirectory) {
        return new UploadFileOperation(this.postConnectOperation,
                                       file,
                                       remoteDirectory,
                                       this.tempFileExtension,
                                       UploadFileOperation.TransferStrategy.RESUME_AFTER_FIRST_ATTEMPT,
                                       this.maxAttempts,
                                       this.attemptPeriod,
                                       false,
                                       this);
    }

    private void runFileOperations()
            throws Exception {
        for (FileTransferOperation operation : this.fileOperations.values()) {
            run(operation);
        }
    }

    private void run(FileTransferOperation operation)
            throws Exception {
        try {
            operation.applyTo(this.transfer);
        } catch (TransferOperationCancelledException e) {
            // ignore
        }
    }

}
