package org.jboss.windup.decompiler.decompiler;

import org.jboss.windup.decompiler.api.ClassDecompileRequest;
import org.jboss.windup.decompiler.api.DecompilationException;
import org.jboss.windup.decompiler.api.DecompilationListener;
import org.jboss.windup.decompiler.api.DecompilationResult;
import org.jboss.windup.decompiler.api.Decompiler;
import org.jboss.windup.decompiler.util.Filter;
import org.jboss.windup.util.Checks;
import org.jboss.windup.util.threading.WindupExecutors;

import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;

import org.jboss.windup.util.exception.WindupStopException;

/**
 * An abstract class encapsulating the common logic from the {@link org.jboss.windup.decompiler.api.Decompiler} implementations.
 *
 * @author <a href="mailto:mbriskar@redhat.com">Matej Briskar</a>
 */
public abstract class AbstractDecompiler implements Decompiler {
    private ExecutorService executorService = WindupExecutors.newSingleThreadExecutor();
    private int numberOfThreads = 1;

    public abstract Logger getLogger();

    public void setExecutorService(ExecutorService service, int numberOfThreads) {
        this.executorService.shutdown();
        try {
            executorService.awaitTermination(10, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new IllegalStateException("Was not able to shutdown the compilation ExecutorService in ten minutes.");
        }
        this.numberOfThreads = numberOfThreads;
        this.executorService = service;
    }

    protected Map<String, List<ClassDecompileRequest>> groupDecompileRequests(final Collection<ClassDecompileRequest> requests) {
        Map<String, List<ClassDecompileRequest>> requestMap = new HashMap<>();
        for (ClassDecompileRequest request : requests) {

            /*
             * Combine requests that are related (for example Foo.class and Foo$1.class), as this helps fernflower to resolve inner classes.
             */
            String filename = request.getClassFile().getFileName().toString();
            String key;
            boolean mainClassFile = false;
            if (filename.matches(".*\\$.*.class")) {
                key = request.getClassFile().getParent().resolve(filename.substring(0, filename.indexOf("$")) + ".class").toString();
            } else {
                mainClassFile = true;
                key = request.getClassFile().toString();
            }

            List<ClassDecompileRequest> list = requestMap.get(key);
            if (list == null) {
                list = new ArrayList<>();
                requestMap.put(key, list);
            }
            if (mainClassFile) {
                list.add(0, request);
            } else {
                list.add(request);
            }
        }
        return requestMap;
    }

    public abstract Collection<Callable<File>> getDecompileTasks(Map<String, List<ClassDecompileRequest>> requestMap, DecompilationListener listener);

    @Override
    public void decompileClassFiles(Collection<ClassDecompileRequest> requests, DecompilationListener listener) {
        Map<String, List<ClassDecompileRequest>> requestMap = groupDecompileRequests(requests);
        Collection<Callable<File>> tasks = getDecompileTasks(requestMap, listener);
        try {
            List<Future<File>> futures = executorService.invokeAll(tasks);
            futures.forEach(f -> {
                try {
                    f.get();
                } catch (WindupStopException ex) {
                    // The execution has stopped on request.
                } catch (InterruptedException | ExecutionException ex) {
                    // Ignore, this was already handled by the listener.
                }
            });
        } catch (InterruptedException e) {
            throw new IllegalStateException("Decompilation was interrupted.");
        } finally {
            listener.decompilationProcessComplete();
        }
    }

    @Override
    public void close() {
        this.executorService.shutdown();
        try {
            executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            throw new IllegalStateException("Was not able to decompile in the given time limit.");
        }
    }

    /**
     * Decompiles all .class files and nested archives in the given archive.
     * <p>
     * Nested archives will be decompiled into directories matching the name of the archive, e.g.
     * <code>foo.ear/bar.jar/src/com/foo/bar/Baz.java</code>.
     * <p>
     * Required directories will be created as needed.
     *
     * @param archive   The archive containing source files and archives.
     * @param outputDir The directory where decompiled .java files will be placed.
     * @returns Result with all decompilation failures. Never throws.
     */
    @Override
    public DecompilationResult decompileArchive(Path archive, Path outputDir, DecompilationListener listener) throws DecompilationException {
        return decompileArchive(archive, outputDir, null, listener);
    }

    @Override
    public DecompilationResult decompileArchive(Path archive, Path outputDir, Filter<ZipEntry> filter, DecompilationListener listener)
            throws DecompilationException {
        Checks.checkFileToBeRead(archive.toFile(), "Archive to decompile");
        Checks.checkDirectoryToBeFilled(outputDir.toFile(), "Output directory");
        return decompileArchiveImpl(archive, outputDir, filter, listener);
    }

    public abstract DecompilationResult decompileArchiveImpl(Path archive, Path outputDir, Filter<ZipEntry> filter, DecompilationListener listener);

    public ExecutorService getExecutorService() {
        return executorService;
    }

    public int getNumberOfThreads() {
        return numberOfThreads;
    }
}
