/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2013-2015 ForgeRock AS.
 */
package org.forgerock.opendj.maven;

import static org.apache.maven.plugins.annotations.LifecyclePhase.*;
import static org.apache.maven.plugins.annotations.ResolutionScope.*;

import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;

/**
 * Generate configuration classes from XML definition files for OpenDJ server.
 * <p>
 * There is a single goal that generate java sources, manifest files, I18N
 * messages and cli/ldap profiles. Resources will be looked for in the following
 * places depending on whether the plugin is executing for the core config or an
 * extension:
 * <table border="1">
 * <tr>
 * <th></th>
 * <th>Location</th>
 * </tr>
 * <tr>
 * <th align="left">XSLT stylesheets</th>
 * <td>Internal: /config/stylesheets</td>
 * </tr>
 * <tr>
 * <th align="left">XML core definitions</th>
 * <td>Internal: /config/xml</td>
 * </tr>
 * <tr>
 * <th align="left">XML extension definitions</th>
 * <td>${basedir}/src/main/java</td>
 * </tr>
 * <tr>
 * <th align="left">Generated Java APIs</th>
 * <td>${project.build.directory}/generated-sources/config</td>
 * </tr>
 * <tr>
 * <th align="left">Generated I18N messages</th>
 * <td>${project.build.outputDirectory}/config/messages</td>
 * </tr>
 * <tr>
 * <th align="left">Generated profiles</th>
 * <td>${project.build.outputDirectory}/config/profiles/${profile}</td>
 * </tr>
 * <tr>
 * <th align="left">Generated manifest</th>
 * <td>${project.build.outputDirectory}/META-INF/services/org.forgerock.opendj.
 * config.AbstractManagedObjectDefinition</td>
 * </tr>
 * </table>
 */
@Mojo(name = "generate-config", defaultPhase = GENERATE_SOURCES, requiresDependencyResolution = COMPILE_PLUS_RUNTIME)
public final class GenerateConfigMojo extends AbstractMojo {

    private interface StreamSourceFactory {
        StreamSource newStreamSource() throws IOException;
    }

    /**
     * The Maven Project.
     */
    @Parameter(required = true, readonly = true, property = "project")
    private MavenProject project;

    /**
     * Package name for which artifacts are generated.
     * <p>
     * This relative path is used to locate xml definition files and to locate
     * generated artifacts.
     */
    @Parameter(required = true)
    private String packageName;

    /**
     * Package name for which artifacts are generated.
     * <p>
     * This relative path is used to locate xml definition files and to locate
     * generated artifacts.
     */
    @Parameter(required = true, defaultValue = "true")
    private Boolean isExtension;

    private final Map<String, StreamSourceFactory> componentDescriptors = new LinkedHashMap<>();
    private TransformerFactory stylesheetFactory;
    private Templates stylesheetMetaJava;
    private Templates stylesheetServerJava;
    private Templates stylesheetClientJava;
    private Templates stylesheetMetaPackageInfo;
    private Templates stylesheetServerPackageInfo;
    private Templates stylesheetClientPackageInfo;
    private Templates stylesheetProfileLDAP;
    private Templates stylesheetProfileCLI;
    private Templates stylesheetMessages;
    private Templates stylesheetManifest;
    private final Queue<Future<?>> tasks = new LinkedList<>();

    private final URIResolver resolver = new URIResolver() {

        @Override
        public synchronized Source resolve(final String href, final String base)
                throws TransformerException {
            if (href.endsWith(".xsl")) {
                final String stylesheet;
                if (href.startsWith("../")) {
                    stylesheet = "/config/stylesheets/" + href.substring(3);
                } else {
                    stylesheet = "/config/stylesheets/" + href;
                }
                getLog().debug("#### Resolved stylesheet " + href + " to " + stylesheet);
                return new StreamSource(getClass().getResourceAsStream(stylesheet));
            } else if (href.endsWith(".xml")) {
                if (href.startsWith("org/forgerock/opendj/server/config/")) {
                    final String coreXML = "/config/xml/" + href;
                    getLog().debug("#### Resolved core XML definition " + href + " to " + coreXML);
                    return new StreamSource(getClass().getResourceAsStream(coreXML));
                } else {
                    final String extXML = getXMLDirectory() + "/" + href;
                    getLog().debug(
                            "#### Resolved extension XML definition " + href + " to " + extXML);
                    return new StreamSource(new File(extXML));
                }
            } else {
                throw new TransformerException("Unable to resolve URI " + href);
            }
        }
    };

    @Override
    public void execute() throws MojoExecutionException {
        if (getPackagePath() == null) {
            throw new MojoExecutionException("<packagePath> must be set.");
        } else if (!isXMLPackageDirectoryValid()) {
            throw new MojoExecutionException("The XML definition directory \""
                    + getXMLPackageDirectory() + "\" does not exist.");
        } else if (getClass().getResource(getStylesheetDirectory()) == null) {
            throw new MojoExecutionException("The XSLT stylesheet directory \""
                    + getStylesheetDirectory() + "\" does not exist.");
        }

        // Validate and transform.
        try {
            initializeStylesheets();
            loadXMLDescriptors();
            executeValidateXMLDefinitions();
            executeTransformXMLDefinitions();
            getLog().info("Adding source directory \"" + getGeneratedSourcesDirectory() + "\" to build path...");
            project.addCompileSourceRoot(getGeneratedSourcesDirectory());
            project.addResource(getGeneratedMavenResources());
        } catch (final Exception e) {
            throw new MojoExecutionException("XSLT configuration transformation failed", e);
        }
    }

    private Resource getGeneratedMavenResources() {
        final String[] generatedResourcesRelativePath =
                new String[] { "/META-INF/services/**", "/config/**/*.properties" };
        final Resource resources = new Resource();
        resources.setDirectory(getGeneratedResourcesDirectory());
        for (final String generatedResourceRelativePath : generatedResourcesRelativePath) {
            resources.addInclude(generatedResourceRelativePath);
            getLog().info("Adding resource \"" + getGeneratedResourcesDirectory() + generatedResourceRelativePath
                    + " to resource path...");
        }

        return resources;
    }

    private void createTransformTask(final StreamSourceFactory inputFactory, final StreamResult output,
            final Templates stylesheet, final ExecutorService executor, final String... parameters)
            throws Exception {
        final Future<Void> future = executor.submit(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                final Transformer transformer = stylesheet.newTransformer();
                transformer.setURIResolver(resolver);
                for (int i = 0; i < parameters.length; i += 2) {
                    transformer.setParameter(parameters[i], parameters[i + 1]);
                }
                transformer.transform(inputFactory.newStreamSource(), output);
                return null;
            }
        });
        tasks.add(future);
    }

    private void createTransformTask(final StreamSourceFactory inputFactory,
            final String outputFileName, final Templates stylesheet,
            final ExecutorService executor, final String... parameters) throws Exception {
        final File outputFile = new File(outputFileName);
        outputFile.getParentFile().mkdirs();
        final StreamResult output = new StreamResult(outputFile);
        createTransformTask(inputFactory, output, stylesheet, executor, parameters);
    }

    private void executeTransformXMLDefinitions() throws Exception {
        getLog().info("Transforming XML definitions...");

        /*
         * Restrict the size of the thread pool in order to throttle
         * creation of transformers and ZIP input streams and prevent potential
         * OOME.
         */
        final ExecutorService parallelExecutor = Executors.newFixedThreadPool(16);

        /*
         * The manifest is a single file containing the concatenated output of
         * many transformations. Therefore we must ensure that output is
         * serialized by using a single threaded executor.
         */
        final ExecutorService sequentialExecutor = Executors.newSingleThreadExecutor();
        final File manifestFile = new File(getGeneratedManifestFile());
        manifestFile.getParentFile().mkdirs();
        final FileOutputStream manifestFileOutputStream = new FileOutputStream(manifestFile);
        final StreamResult manifest = new StreamResult(manifestFileOutputStream);
        try {
            /*
             * Generate Java classes and resources for each XML definition.
             */
            final String javaDir = getGeneratedSourcesDirectory() + "/" + getPackagePath() + "/";
            final String metaDir = javaDir + "meta/";
            final String serverDir = javaDir + "server/";
            final String clientDir = javaDir + "client/";
            final String ldapProfileDir =
                    getGeneratedProfilesDirectory("ldap") + "/" + getPackagePath() + "/meta/";
            final String cliProfileDir =
                    getGeneratedProfilesDirectory("cli") + "/" + getPackagePath() + "/meta/";
            final String i18nDir =
                    getGeneratedMessagesDirectory() + "/" + getPackagePath() + "/meta/";

            for (final Map.Entry<String, StreamSourceFactory> entry : componentDescriptors
                    .entrySet()) {
                final String meta = metaDir + entry.getKey() + "CfgDefn.java";
                createTransformTask(entry.getValue(), meta, stylesheetMetaJava, parallelExecutor);

                final String server = serverDir + entry.getKey() + "Cfg.java";
                createTransformTask(entry.getValue(), server, stylesheetServerJava,
                        parallelExecutor);

                final String client = clientDir + entry.getKey() + "CfgClient.java";
                createTransformTask(entry.getValue(), client, stylesheetClientJava,
                        parallelExecutor);

                final String ldap = ldapProfileDir + entry.getKey() + "CfgDefn.properties";
                createTransformTask(entry.getValue(), ldap, stylesheetProfileLDAP, parallelExecutor);

                final String cli = cliProfileDir + entry.getKey() + "CfgDefn.properties";
                createTransformTask(entry.getValue(), cli, stylesheetProfileCLI, parallelExecutor);

                final String i18n = i18nDir + entry.getKey() + "CfgDefn.properties";
                createTransformTask(entry.getValue(), i18n, stylesheetMessages, parallelExecutor);

                createTransformTask(entry.getValue(), manifest, stylesheetManifest,
                        sequentialExecutor);
            }

            // Generate package-info.java files.
            final Map<String, Templates> profileMap = new LinkedHashMap<>();
            profileMap.put("meta", stylesheetMetaPackageInfo);
            profileMap.put("server", stylesheetServerPackageInfo);
            profileMap.put("client", stylesheetClientPackageInfo);
            for (final Map.Entry<String, Templates> entry : profileMap.entrySet()) {
                final StreamSourceFactory sourceFactory = new StreamSourceFactory() {
                    @Override
                    public StreamSource newStreamSource() throws IOException {
                        if (isExtension) {
                            return new StreamSource(new File(getXMLPackageDirectory()
                                    + "/Package.xml"));
                        } else {
                            return new StreamSource(getClass().getResourceAsStream(
                                    "/" + getXMLPackageDirectory() + "/Package.xml"));
                        }
                    }
                };
                final String profile = javaDir + "/" + entry.getKey() + "/package-info.java";
                createTransformTask(sourceFactory, profile, entry.getValue(), parallelExecutor,
                        "type", entry.getKey());
            }

            /*
             * Wait for all transformations to complete and cleanup. Remove the
             * completed tasks from the list as we go in order to free up
             * memory.
             */
            for (Future<?> task = tasks.poll(); task != null; task = tasks.poll()) {
                task.get();
            }
        } finally {
            parallelExecutor.shutdown();
            sequentialExecutor.shutdown();
            manifestFileOutputStream.close();
        }
    }

    private void executeValidateXMLDefinitions() {
        // TODO:
        getLog().info("Validating XML definitions...");
    }

    private String getBaseDir() {
        return project.getBasedir().toString();
    }

    private String getGeneratedResourcesDirectory() {
        return project.getBuild().getDirectory() + "/generated-resources";
    }

    private String getGeneratedManifestFile() {
        return getGeneratedResourcesDirectory()
                + "/META-INF/services/org.forgerock.opendj.config.AbstractManagedObjectDefinition";
    }

    private String getGeneratedMessagesDirectory() {
        return getGeneratedResourcesDirectory() + "/config/messages";
    }

    private String getGeneratedProfilesDirectory(final String profileName) {
        return getGeneratedResourcesDirectory() + "/config/profiles/" + profileName;
    }

    private String getGeneratedSourcesDirectory() {
        return project.getBuild().getDirectory() + "/generated-sources/config";
    }

    private String getPackagePath() {
        return packageName.replace('.', '/');
    }

    private String getStylesheetDirectory() {
        return "/config/stylesheets";
    }

    private String getXMLDirectory() {
        if (isExtension) {
            return getBaseDir() + "/src/main/java";
        } else {
            return "config/xml";
        }
    }

    private String getXMLPackageDirectory() {
        return getXMLDirectory() + "/" + getPackagePath();
    }

    private void initializeStylesheets() throws TransformerConfigurationException {
        getLog().info("Loading XSLT stylesheets...");
        stylesheetFactory = TransformerFactory.newInstance();
        stylesheetFactory.setURIResolver(resolver);
        stylesheetMetaJava = loadStylesheet("metaMO.xsl");
        stylesheetMetaPackageInfo = loadStylesheet("package-info.xsl");
        stylesheetServerJava = loadStylesheet("serverMO.xsl");
        stylesheetServerPackageInfo = loadStylesheet("package-info.xsl");
        stylesheetClientJava = loadStylesheet("clientMO.xsl");
        stylesheetClientPackageInfo = loadStylesheet("package-info.xsl");
        stylesheetProfileLDAP = loadStylesheet("ldapMOProfile.xsl");
        stylesheetProfileCLI = loadStylesheet("cliMOProfile.xsl");
        stylesheetMessages = loadStylesheet("messagesMO.xsl");
        stylesheetManifest = loadStylesheet("manifestMO.xsl");
    }

    private boolean isXMLPackageDirectoryValid() {
        // Not an extension, so always valid.
        return !isExtension
            || new File(getXMLPackageDirectory()).isDirectory();
    }

    private Templates loadStylesheet(final String stylesheet)
            throws TransformerConfigurationException {
        final Source xslt =
                new StreamSource(getClass().getResourceAsStream(
                        getStylesheetDirectory() + "/" + stylesheet));
        return stylesheetFactory.newTemplates(xslt);
    }

    private void loadXMLDescriptors() throws IOException {
        getLog().info("Loading XML descriptors...");
        final String parentPath = getXMLPackageDirectory();
        final String configFileName = "Configuration.xml";
        if (isExtension) {
            final File dir = new File(parentPath);
            dir.listFiles(new FileFilter() {
                @Override
                public boolean accept(final File path) {
                    final String name = path.getName();
                    if (path.isFile() && name.endsWith(configFileName)) {
                        final String key = name.substring(0, name.length() - configFileName.length());
                        componentDescriptors.put(key, new StreamSourceFactory() {
                            @Override
                            public StreamSource newStreamSource() {
                                return new StreamSource(path);
                            }
                        });
                    }
                    return true; // Don't care about the result.
                }
            });
        } else {
            final URL dir = getClass().getClassLoader().getResource(parentPath);
            final JarURLConnection connection = (JarURLConnection) dir.openConnection();
            final JarFile jar = connection.getJarFile();
            final Enumeration<JarEntry> entries = jar.entries();
            while (entries.hasMoreElements()) {
                final JarEntry entry = entries.nextElement();
                final String name = entry.getName();
                if (name.startsWith(parentPath) && name.endsWith(configFileName)) {
                    final int startPos = name.lastIndexOf('/') + 1;
                    final int endPos = name.length() - configFileName.length();
                    final String key = name.substring(startPos, endPos);
                    componentDescriptors.put(key, new StreamSourceFactory() {
                        @Override
                        public StreamSource newStreamSource() throws IOException {
                            return new StreamSource(jar.getInputStream(entry));
                        }
                    });
                }
            }
        }
        getLog().info("Found " + componentDescriptors.size() + " XML descriptors");
    }
}
