package com.jscape.filetransfer;

import com.jscape.util.Attemptable;
import com.jscape.util.FileTools;
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 DownloadDirectoryOperation
        implements FileTransferOperation,
                   DownloadFileOperation.Listener {

    private final FileTransferOperation postConnectOperation;
    private final String remoteDirectory;
    private final int maxAttempts;
    private final Time attemptPeriod;
    private final String fileSeparator;
    private final int threadCount;
    private FileTransfer transfer;
    private boolean disconnectOnCompleted;
    private Map<String, DownloadFileOperation> fileOperations;
    private ExecutorService executorService;
    private BlockingQueue<FileTransfer> transfers;
    private Tree<DirectoryService.Directory<FileTransferRemoteFile>> remoteDirectoryTree;
    private CountDownLatch doneSignal;
    private volatile boolean cancelled;

    public DownloadDirectoryOperation(FileTransferOperation postConnectOperation,
                                      String remoteDirectory,
                                      int maxAttempts,
                                      Time attemptPeriod,
                                      String fileSeparator,
                                      Integer threadCount) {
        assertNotNull(postConnectOperation);
        this.postConnectOperation = postConnectOperation;

        assertNotNull(remoteDirectory);
        this.remoteDirectory = remoteDirectory;

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

        assertNotNull(attemptPeriod);
        this.attemptPeriod = attemptPeriod;

        assertNotNull(fileSeparator);
        this.fileSeparator = fileSeparator;

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

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

        this.cancelled = false;

        try {
            initRemoteDirectoryTree();
            createLocalDirectoryTree();
            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 (DownloadFileOperation operation : this.fileOperations.values()) {
            operation.cancel();
        }
    }

    public void cancel(String file) {
        DownloadFileOperation operation = findOperationFor(file);

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

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

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

    @Override
    public String toString() {
        return "DownloadDirectoryOperation {"
                + "postConnectOperation=" + this.postConnectOperation
                + ", remoteDirectory='" + this.remoteDirectory + '\''
                + ", maxAttempts=" + this.maxAttempts
                + ", attemptPeriod=" + this.attemptPeriod
                + ", fileSeparator='" + this.fileSeparator + '\''
                + ", threadCount=" + this.threadCount
                + ", remoteDirectoryTree=" + this.remoteDirectoryTree
                + ", cancelled=" + this.cancelled
                + '}';
    }

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

    private void initRemoteDirectoryTree()
            throws Exception {
        Attemptable.run(new Callable<Object>() {
            @Override
            public Object call()
                    throws Exception {
                try {
                    initTransfer();
                    remoteDirectoryTree = DirectoryService.asTree(remoteDirectory, transfer);
                    return null;
                } catch (Exception e) {
                    disconnect();
                    throw e;
                }
            }
        }, this.attemptPeriod.toMillis(), this.maxAttempts);
    }

    private void createLocalDirectoryTree()
            throws Exception {
        createLocalDirectoryTree(this.transfer.getLocalDir(), this.remoteDirectoryTree);
    }

    private void initFileOperations()
            throws Exception {
        this.fileOperations = new HashMap<>();
        LinkedList<String> remoteDirectoryPath = currentRemotePath();
        initFileOperations(remoteDirectoryPath, this.remoteDirectoryTree, this.transfer.getLocalDir());
    }

    private LinkedList<String> currentRemotePath()
            throws Exception {
        LinkedList<String> path = new LinkedList<>();
        path.add(this.transfer.getDir());
        return path;
    }

    private void initFileOperations(List<String> remoteDirectoryPath,
                                    Tree<DirectoryService.Directory<FileTransferRemoteFile>> tree,
                                    File parentLocalDirectory) {
        String directoryPath = tree.value().path.getFilename();
        remoteDirectoryPath.add(directoryPath);
        File localDirectory = new File(parentLocalDirectory, nameOf(directoryPath));

        initFileOperations(remoteDirectoryPath, tree.value().files, localDirectory);

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

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

    private void createLocalDirectoryTree(File localDirectory, Tree<DirectoryService.Directory<FileTransferRemoteFile>> tree)
            throws Exception {
        String directoryName = nameOf(tree.value().path.getFilename());
        File subDirectory = new File(localDirectory, directoryName);
        FileTools.createDirectory(subDirectory);

        for (Tree<DirectoryService.Directory<FileTransferRemoteFile>> leaf : tree.leafs()) {
            createLocalDirectoryTree(subDirectory, leaf);
        }
    }

    private String nameOf(String path) {
        if (path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }

        int index = path.lastIndexOf('/');

        return index >= 0 ? path.substring(index + 1) : path;
    }

    private void initFileOperations(List<String> remoteDirectoryPath,
                                    Collection<FileTransferRemoteFile> files,
                                    File localDirectory) {
        RemoteDirectory remoteDirectory = new RemoteDirectory.PathElementsDirectory(remoteDirectoryPath);

        for (FileTransferRemoteFile file : files) {
            String filename = file.getFilename();
            DownloadFileOperation operation = operationFor(remoteDirectory, new File(localDirectory, filename));
            this.fileOperations.put(fullRemotePathFor(remoteDirectoryPath, filename), operation);
        }
    }

    private String fullRemotePathFor(List<String> remoteDirectory, String filename) {
        StringBuilder buffer = new StringBuilder();

        for (String element : remoteDirectory) {
            buffer.append(element);

            if (!element.endsWith(this.fileSeparator)) {
                buffer.append(this.fileSeparator);
            }
        }

        buffer.append(filename);

        return buffer.toString();
    }

    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;
        }
        
        closeTransfers();

        this.remoteDirectoryTree = null;
        this.doneSignal = null;

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

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

    private DownloadFileOperation findOperationFor(String file) {
        return this.fileOperations.get(file);
    }

    private DownloadFileOperation operationFor(RemoteDirectory remoteDirectory, File file) {
        return new DownloadFileOperation(this.postConnectOperation,
                                         remoteDirectory,
                                         null,
                                         file,
                                         DownloadFileOperation.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
        }
    }
}
