package com.jscape.filetransfer;

import com.jscape.util.Attemptable;
import com.jscape.util.Clock;
import com.jscape.util.Time;
import com.jscape.util.collections.EnumerationIterator;
import com.jscape.util.regex.Matcher;

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

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

/**
 * @author Alex Usun
 */
public class DownloadRegexOperation
        implements FileTransferOperation,
                   DownloadFileOperation.Listener {

    private final FileTransferOperation postConnectOperation;
    private final String remoteDirectory;
    private final Matcher matcher;
    private final long latencyPeriod;
    private final boolean failIfNoFilesFound;
    private final int maxAttempts;
    private final Time attemptPeriod;
    private final String fileSeparator;
    private final int threadCount;
    private final boolean deleteRemoteFiles;
    private FileTransfer transfer;
    private boolean disconnectOnCompleted;
    private Map<String, DownloadFileOperation> fileOperations;
    private List<File> localFiles;
    private ExecutorService executorService;
    private BlockingQueue<FileTransfer> transfers;
    private CountDownLatch doneSignal;
    private volatile boolean cancelled;

    public DownloadRegexOperation(FileTransferOperation postConnectOperation,
                                  String remoteDirectory,
                                  Matcher matcher,
                                  long latencyPeriod,
                                  boolean failIfNoFilesFound,
                                  int maxAttempts,
                                  Time attemptPeriod,
                                  String fileSeparator,
                                  Integer threadCount,
                                  boolean deleteRemoteFiles) {
        assertNotNull(postConnectOperation);
        this.postConnectOperation = postConnectOperation;

        assertNotNull(remoteDirectory);
        this.remoteDirectory = remoteDirectory;

        assertNotNull(matcher);
        this.matcher = matcher;

        this.latencyPeriod = latencyPeriod;
        this.failIfNoFilesFound = failIfNoFilesFound;

        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;

        this.deleteRemoteFiles = deleteRemoteFiles;
    }

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

        this.cancelled = false;

        try {
            init();
            initDoneSignal();

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

            awaitOperationsDone();
            assertNotCancelled();

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

    private void init()
            throws Exception {
        Attemptable.run(() -> {
            try {
                initTransfer();
                initFileOperations();
                return null;
            } catch (Exception e) {
                disconnect();
                throw e;
            }
        }, this.attemptPeriod.toMillis(), this.maxAttempts);

    }

    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 List<File> files() {
        return this.localFiles != null ? this.localFiles : Collections.emptyList();
    }

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

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

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

        LinkedList<String> remoteDirectoryPath = currentRemotePath();
        RemoteDirectory remoteDirectory = new RemoteDirectory.PathElementsDirectory(remoteDirectoryPath);

        List<FileTransferRemoteFile> files = filesToDownload(remoteDirectory);
        this.localFiles = new ArrayList<>(files.size());

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

    private List<FileTransferRemoteFile> filesToDownload(RemoteDirectory remoteDirectory)
            throws Exception {
        remoteDirectory.applyTo(this.transfer);

        List<FileTransferRemoteFile> files = new LinkedList<>();

        for (FileTransferRemoteFile file : EnumerationIterator.<FileTransferRemoteFile>iterable(this.transfer.getDirListing())) {
            if (!file.isDirectory() && this.matcher.matches(file.getFilename()) && latencyPeriodExceeded(file.getFileDate().getTime())) {
                files.add(file);
            }
        }

        if (this.failIfNoFilesFound && files.isEmpty()) {
            throw new Exception("No files matched regular expression found.");
        }

        return files;
    }

    private boolean latencyPeriodExceeded(long fileLastModificationTime) {
        return this.latencyPeriod == 0 || Clock.elapsedSince(fileLastModificationTime) > this.latencyPeriod;
    }

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

        if (this.remoteDirectory != null && !this.remoteDirectory.isEmpty()) {
            path.add(this.remoteDirectory);
        }

        return path;
    }

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

    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;
        }

        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,
                                         this.deleteRemoteFiles,
                                         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
        }
    }
}
