package io.reactiverse.vertx.maven.plugin.components.impl;

import io.reactiverse.vertx.maven.plugin.components.*;
import io.reactiverse.vertx.maven.plugin.mojos.Archive;
import io.reactiverse.vertx.maven.plugin.mojos.DependencySet;
import io.reactiverse.vertx.maven.plugin.mojos.FileItem;
import io.reactiverse.vertx.maven.plugin.mojos.FileSet;
import org.apache.commons.io.IOUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.artifact.filter.resolve.ScopeFilter;
import org.apache.maven.shared.artifact.filter.resolve.transform.ArtifactIncludeFilterTransformer;
import org.apache.maven.shared.utils.io.DirectoryScanner;
import org.apache.maven.shared.utils.io.SelectorUtils;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.util.FileUtils;
import org.jboss.shrinkwrap.api.ArchivePath;
import org.jboss.shrinkwrap.api.Node;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.ByteArrayAsset;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.importer.ZipImporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;

import java.io.*;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

/**
 * @author <a href="http://escoffier.me">Clement Escoffier</a>
 */
@Component(
    role = PackageService.class,
    hint = "fat-jar")
public class ShrinkWrapFatJarPackageService implements PackageService {

    private static final List<String> DEFAULT_EXCLUDES;

    static {
        DEFAULT_EXCLUDES = new ArrayList<>(FileUtils.getDefaultExcludesAsList());
        DEFAULT_EXCLUDES.add("**/*.DSA");
        DEFAULT_EXCLUDES.add("**/*.RSA");
        DEFAULT_EXCLUDES.add("**/INDEX.LIST");
        DEFAULT_EXCLUDES.add("**/*.SF");
    }

    @Override
    public PackageType type() {
        return PackageType.FAT_JAR;
    }

    @Override
    public File doPackage(PackageConfig config) throws
        PackagingException {

        Log logger = Objects.requireNonNull(config.getMojo().getLog());
        Archive archive = Objects.requireNonNull(config.getArchive());

        JavaArchive jar = ShrinkWrap.create(JavaArchive.class);

        for (DependencySet ds : archive.getDependencySets()) {
            ScopeFilter scopeFilter = ServiceUtils.newScopeFilter(ds.getScope());
            ArtifactFilter filter = new ArtifactIncludeFilterTransformer().transform(scopeFilter);
            Set<Artifact> artifacts = ServiceUtils.filterArtifacts(config.getArtifacts(),
                ds.getIncludes(), ds.getExcludes(),
                ds.isUseTransitiveDependencies(), logger, filter);

            // Add dependencies
            for (Artifact artifact : artifacts) {
                File file = artifact.getFile();
                if (file.isFile()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Adding Dependency :" + artifact);
                    }
                    embedDependency(logger, ds, jar, file);
                } else {
                    logger.info("Cannot embed artifact " + artifact
                        + " - the file does not exist");
                }
            }
        }

        for (FileSet fs : archive.getFileSets()) {
            embedFileSet(logger, config.getProject(), fs, jar);
        }

        // Add classes
        if (archive.isIncludeClasses()) {
            File classes = new File(config.getProject().getBuild().getOutputDirectory());
            if (classes.isDirectory()) {
                jar.addAsResource(classes, "/");
            }
        }

        // File Items
        for (FileItem item : archive.getFiles()) {
            embedFile(config, jar, item);
        }

        // Generate manifest
        try {
            generateManifest(jar, archive.getManifest());
        } catch (IOException | MojoExecutionException e) {
            throw new PackagingException(e);
        }

        // Generate output file
        File jarFile;

        try {
            jarFile = config.getOutput();

            boolean useTmpFile = false;
            File theCreatedFile = jarFile;
            if (jarFile.isFile()) {
                useTmpFile = true;
                theCreatedFile = new File(jarFile.getParentFile(), jarFile.getName() + ".tmp");
            }

            jar.as(ZipExporter.class).exportTo(theCreatedFile);

            if (useTmpFile) {
                boolean delete = jarFile.delete();
                boolean renameTo = theCreatedFile.renameTo(jarFile);
                config.getMojo().getLog().debug("Main jar file deleted: " + delete);
                config.getMojo().getLog().debug("Main jar file replaced by temporary file: " + renameTo);
            }

        } catch (Exception e) {
            throw new PackagingException(e);
        }

        return jarFile;
    }

    private void embedFile(PackageConfig config, JavaArchive jar, FileItem item) throws PackagingException {
        String path;
        if (item.getOutputDirectory() == null) {
            path = "/";
        } else {
            path = item.getOutputDirectory();
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (!path.endsWith("/")) {
                path = path + "/";
            }
        }

        File source = new File(config.getProject().getBasedir(), item.getSource());
        if (!source.isFile()) {
            Node node = jar.get(item.getSource());
            if (node == null) {
                throw new PackagingException("Unable to handle the file item " + item.getSource() + ", " +
                    "file not found in the project or in the archive.");
            }

            String name = item.getDestName();
            if (name == null) {
                name = node.getPath().get().substring(node.getPath().getParent().get().length() + 1);
            }

            String out = path + name;
            jar.add(node.getAsset(), out);
            jar.delete(node.getPath());
        } else {
            String name = item.getDestName();
            if (name == null) {
                name = source.getName();
            }
            String out = path + name;
            jar.addAsResource(source, out);
        }
    }

    private void embedFileSet(Log log, MavenProject project, FileSet fs, JavaArchive jar) {
        File directory = new File(fs.getDirectory());
        if (!directory.isAbsolute()) {
            directory = new File(project.getBasedir(), fs.getDirectory());
        }

        if (!directory.isDirectory()) {
            log.warn("File set root directory (" + directory.getAbsolutePath() + ") does not exist " +
                "- skipping");
            return;
        }

        DirectoryScanner scanner = new DirectoryScanner();
        scanner.setBasedir(directory);
        if (fs.getOutputDirectory() == null) {
            fs.setOutputDirectory("/");
        } else if (!fs.getOutputDirectory().startsWith("/")) {
            fs.setOutputDirectory("/" + fs.getOutputDirectory());
        }
        List<String> excludes = fs.getExcludes();
        if (fs.isUseDefaultExcludes()) {
            excludes.addAll(FileUtils.getDefaultExcludesAsList());
        }
        if (!excludes.isEmpty()) {
            scanner.setExcludes(excludes.toArray(new String[0]));
        }
        if (!fs.getIncludes().isEmpty()) {
            scanner.setIncludes(fs.getIncludes().toArray(new String[0]));
        }
        scanner.scan();
        String[] files = scanner.getIncludedFiles();
        for (String path : files) {
            File file = new File(directory, path);
            log.debug("Adding " + fs.getOutputDirectory() + path + " to the archive");
            jar.addAsResource(file, fs.getOutputDirectory() + path);
        }
    }


    private boolean toExclude(DependencySet set, ArchivePath path) {
        String name = path.get();

        // Check whether the file is explicitly includes
        List<String> includes = set.getOptions().getIncludes();
        if (includes != null && !includes.isEmpty()) {
            boolean included = false;

            // Check for each include pattern whether or not the path is explicitly included
            for (String pattern : includes) {
                if (SelectorUtils.match(pattern, name)) {
                    included = true;
                }
            }

            // If the path is not included, exclude the file
            // otherwise apply the excludes pattern on it.
            if (!included) {
                return true;
            }
        }

        if (set.getOptions().isUseDefaultExcludes()) {
            for (String pattern : DEFAULT_EXCLUDES) {
                if (SelectorUtils.match(pattern, name)) {
                    return true;
                }
            }
        }

        if (name.equalsIgnoreCase("/META-INF/MANIFEST.MF")) {
            return true;
        }

        if (set.getOptions().getExcludes() != null) {
            for (String pattern : set.getExcludes()) {
                if (SelectorUtils.match(pattern, name)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Import from file and make sure the file is closed.
     *
     * @param log  the logger
     * @param set  the dependency set
     * @param jar  the archive
     * @param file the file, must not be {@code null}
     */
    private void embedDependency(Log log, DependencySet set, JavaArchive jar, File file) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            jar.as(ZipImporter.class).importFrom(fis, path -> {
                if (jar.contains(path)) {
                    log.debug(path.get() + " already embedded in the jar");
                    return false;
                }
                if (!toExclude(set, path)) {
                    return true;
                } else {
                    log.debug("Excluding " + path.get() + " from " + file.getName());
                    return false;
                }
            });
        } catch (FileNotFoundException e) {
            throw new RuntimeException("Unable to read the file " + file.getAbsolutePath(), e);
        } finally {
            IOUtils.closeQuietly(fis);
        }
    }

    /**
     * Generate the manifest for the ├╝ber jar.
     */
    private void generateManifest(JavaArchive jar, Map<String, String> entries) throws IOException,
        MojoExecutionException {
        Manifest manifest = new Manifest();
        Attributes attributes = manifest.getMainAttributes();
        attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
        if (entries != null) {
            for (Map.Entry<String, String> entry : entries.entrySet()) {
                attributes.put(new Attributes.Name(entry.getKey()), entry.getValue());
            }
        }

        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        manifest.write(bout);
        bout.close();
        byte[] bytes = bout.toByteArray();
        //TODO: merge existing manifest with current one
        jar.setManifest(new ByteArrayAsset(bytes));

    }
}