/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.maven.plugins.javadoc;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import org.apache.maven.archiver.MavenArchiveConfiguration;
import org.apache.maven.archiver.MavenArchiver;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.reporting.MavenReportException;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.jar.JarArchiver;
import org.codehaus.plexus.archiver.jar.Manifest;
import org.codehaus.plexus.archiver.jar.ManifestException;

/**
 * Bundles the Javadoc documentation for <code>main Java code</code> in an <b>NON aggregator</b> project into a jar
 * using the standard <a href= "http://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/">Javadoc Tool</a>.
 *
 * @version $Id: JavadocJar.java 1752018 2016-07-09 16:35:25Z rfscholte $
 * @since   2.0
 */
@Mojo(name = "jar", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE,
      threadSafe = true)
public class CamelJavadocJar extends AbstractJavadocMojo {
    /**
     * Includes all generated Javadoc files
     */
    private static final String[] DEFAULT_INCLUDES = new String[] { "**/**" };

    /**
     * Excludes all processing files.
     *
     * @see AbstractJavadocMojo#DEBUG_JAVADOC_SCRIPT_NAME
     * @see AbstractJavadocMojo#OPTIONS_FILE_NAME
     * @see AbstractJavadocMojo#PACKAGES_FILE_NAME
     * @see AbstractJavadocMojo#ARGFILE_FILE_NAME
     * @see AbstractJavadocMojo#FILES_FILE_NAME
     */
    private static final String[] DEFAULT_EXCLUDES = new String[] {
            DEBUG_JAVADOC_SCRIPT_NAME, OPTIONS_FILE_NAME, PACKAGES_FILE_NAME, ARGFILE_FILE_NAME, FILES_FILE_NAME };

    // ----------------------------------------------------------------------
    // Mojo components
    // ----------------------------------------------------------------------

    /**
     * Specifies the destination directory where javadoc saves the generated HTML files. <br>
     * 
     * @see <a href= "http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#d">d</a> option
     */
    @Parameter(defaultValue = "${project.build.directory}/apidocstmp", required = true)
    protected File intermediateDirectory;

    /**
     * Used for attaching the artifact in the project.
     */
    @Component
    private MavenProjectHelper projectHelper;

    /**
     * The Jar archiver.
     *
     * @since 2.5
     */
    @Component(role = Archiver.class, hint = "jar")
    private JarArchiver jarArchiver;

    // ----------------------------------------------------------------------
    // Mojo Parameters
    // ----------------------------------------------------------------------

    /**
     * Specifies the destination directory where javadoc saves the generated HTML files. See
     * <a href= "http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#d">d</a>.
     *
     * @deprecated
     */
    @Deprecated
    @Parameter(property = "destDir")
    private File destDir;

    /**
     * Specifies the directory where the generated jar file will be put.
     */
    @Parameter(property = "project.build.directory")
    private String jarOutputDirectory;

    /**
     * Specifies the filename that will be used for the generated jar file. Please note that <code>-javadoc</code> or
     * <code>-test-javadoc</code> will be appended to the file name.
     */
    @Parameter(property = "project.build.finalName")
    private String finalName;

    /**
     * Specifies whether to attach the generated artifact to the project helper. <br/>
     */
    @Parameter(property = "attach", defaultValue = "true")
    private boolean attach;

    /**
     * The archive configuration to use. See <a href="http://maven.apache.org/shared/maven-archiver/index.html">Maven
     * Archiver Reference</a>.
     *
     * @since 2.5
     */
    @Parameter
    private MavenArchiveConfiguration archive = new JavadocArchiveConfiguration() {
        {
            setForced(false);
        }
    };

    /**
     * Path to the default MANIFEST file to use. It will be used if <code>useDefaultManifestFile</code> is set to
     * <code>true</code>.
     *
     * @since 2.5
     */
    @Parameter(defaultValue = "${project.build.outputDirectory}/META-INF/MANIFEST.MF", required = true, readonly = true)
    private File defaultManifestFile;

    /**
     * Set this to <code>true</code> to enable the use of the <code>defaultManifestFile</code>. <br/>
     *
     * @since 2.5
     */
    @Parameter(defaultValue = "false")
    private boolean useDefaultManifestFile;

    /**
     * @since 2.10
     */
    @Parameter(property = "maven.javadoc.classifier", defaultValue = "javadoc", required = true)
    private String classifier;

    /** {@inheritDoc} */
    @Override
    public void doExecute() throws MojoExecutionException {
        if (skip) {
            getLog().info("Skipping javadoc generation");
            return;
        }

        try {
            Field f = AbstractJavadocMojo.class.getDeclaredField("additionalOptions");
            f.setAccessible(true);
            String[] additionalOptions = (String[]) f.get(this);
            if (additionalOptions == null || additionalOptions.length == 0) {
                additionalOptions = new String[] { "-notimestamp" };
            } else {
                List<String> l = new ArrayList<>(Arrays.asList(additionalOptions));
                l.add("-notimestamp");
                additionalOptions = l.toArray(new String[0]);
            }
            f.set(this, additionalOptions);
        } catch (Exception e) {
            throw new MojoExecutionException("Unable to set javadoc options", e);
        }

        File innerDestDir = this.destDir;
        if (innerDestDir == null) {
            innerDestDir = new File(getOutputDirectory());
        }

        if (!("pom".equalsIgnoreCase(project.getPackaging()) && isAggregator())) {
            ArtifactHandler artifactHandler = project.getArtifact().getArtifactHandler();
            if (!"java".equals(artifactHandler.getLanguage())) {
                getLog().info("Not executing Javadoc as the project is not a Java classpath-capable package");
                return;
            }
        }

        try {
            executeReport(Locale.getDefault());
        } catch (MavenReportException e) {
            failOnError("MavenReportException: Error while generating Javadoc", e);
        } catch (RuntimeException e) {
            failOnError("RuntimeException: Error while generating Javadoc", e);
        }

        if (innerDestDir.exists()) {
            try {
                Path inputDir = innerDestDir.toPath();
                Path outputDir = intermediateDirectory.toPath();
                Files.createDirectories(outputDir);
                Set<Path> input = getAllRelativeFiles(inputDir);
                Set<Path> output = getAllRelativeFiles(outputDir);
                // remove deleted files
                output.removeAll(input);
                output.remove(Paths.get("META-INF"));
                output.remove(Paths.get("META-INF/MANIFEST.MF"));
                for (Path p : output) {
                    Files.delete(outputDir.resolve(p));
                }
                // copy all files
                for (Path p : input) {
                    copy(inputDir.resolve(p), outputDir.resolve(p));
                }

                File outputFile = generateArchive(intermediateDirectory, finalName + "-" + getClassifier() + ".jar");

                if (!attach) {
                    getLog().info("NOT adding javadoc to attached artifacts list.");
                } else {
                    // TODO: these introduced dependencies on the project are
                    // going to become problematic - can we export it
                    // through metadata instead?
                    projectHelper.attachArtifact(project, "javadoc", getClassifier(), outputFile);
                }
            } catch (ArchiverException e) {
                failOnError("ArchiverException: Error while creating archive", e);
            } catch (IOException e) {
                failOnError("IOException: Error while creating archive", e);
            } catch (RuntimeException e) {
                failOnError("RuntimeException: Error while creating archive", e);
            }
        }
    }

    /*
     * private void writeIncrementalInfo(MavenProject project) throws
     * MojoExecutionException { try { Path cacheData =
     * getIncrementalDataPath(project); String curdata = getIncrementalData();
     * Files.createDirectories(cacheData.getParent()); try (Writer w =
     * Files.newBufferedWriter(cacheData)) { w.append(curdata); } } catch
     * (IOException e) { throw new
     * MojoExecutionException("Error checking manifest uptodate status", e); } }
     * private boolean isUpToDate(MavenProject project) throws
     * MojoExecutionException { long t0 = System.currentTimeMillis(); try { Path
     * cacheData = getIncrementalDataPath(project); String prvdata; if
     * (Files.isRegularFile(cacheData)) { prvdata = new
     * String(Files.readAllBytes(cacheData), StandardCharsets.UTF_8); } else {
     * prvdata = null; } String curdata = getIncrementalData(); if
     * (curdata.equals(prvdata)) { long lastmod =
     * Files.getLastModifiedTime(cacheData).toMillis(); Set<String> stale =
     * Stream.concat(Stream.of(new
     * File(project.getBuild().getOutputDirectory())),
     * project.getArtifacts().stream().map(Artifact::getFile)) .flatMap(f ->
     * newer(lastmod, f)) .collect(Collectors.toSet()); if (!stale.isEmpty()) {
     * getLog().info("Stale files: " + stale.stream()
     * .collect(Collectors.joining(", "))); } else { // everything is in order,
     * skip
     * getLog().info("Skipping manifest generation, everything is up to date.");
     * return true; } } else { if (prvdata == null) {
     * getLog().info("No previous run data found, generating manifest."); } else
     * { getLog().info("Configuration changed, re-generating manifest."); } } }
     * catch (IOException e) { throw new
     * MojoExecutionException("Error checking manifest uptodate status", e); }
     * finally { long t1 = System.currentTimeMillis();
     * getLog().warn("isUpToDate took " + (t1 - t0) + " ms"); } return false; }
     * private String getIncrementalData() { return
     * getInstructions().entrySet().stream().map(e -> e.getKey() + "=" +
     * e.getValue()) .collect(Collectors.joining("\n", "", "\n")); } private
     * Path getIncrementalDataPath(MavenProject project) { return
     * Paths.get(project.getBuild().getDirectory(), "camel-javadoc-plugin",
     * "org.apache.camel_camel-javadoc-plugin_javadoc_xx"); } private long
     * lastmod(Path p) { try { return Files.getLastModifiedTime(p).toMillis(); }
     * catch (IOException e) { return 0; } } private Stream<String> newer(long
     * lastmod, File file) { try { if (file.isDirectory()) { return
     * Files.walk(file.toPath()) .filter(Files::isRegularFile) .filter(p ->
     * lastmod(p) > lastmod) .map(Path::toString); } else if (file.isFile()) {
     * if (lastmod(file.toPath()) > lastmod) { if
     * (file.getName().endsWith(".jar")) { try (ZipFile zf = new ZipFile(file))
     * { return zf.stream() .filter(ze -> !ze.isDirectory()) .filter(ze ->
     * ze.getLastModifiedTime().toMillis() > lastmod) .map(ze -> file.toString()
     * + "!" + ze.getName()) .collect(Collectors.toList()) .stream(); } } else {
     * return Stream.of(file.toString()); } } else { return Stream.empty(); } }
     * else { return Stream.empty(); } } catch (IOException e) { throw new
     * IOError(e); } }
     */

    void copy(Path in, Path out) throws IOException {
        if (Files.isDirectory(in)) {
            Files.createDirectories(out);
        } else if (Files.isRegularFile(in)) {
            byte[] dataIn = Files.readAllBytes(in);
            if (Files.isRegularFile(out) && Files.isReadable(out)) {
                byte[] dataOut = Files.readAllBytes(out);
                if (Arrays.equals(dataIn, dataOut)) {
                    return;
                }
            }
            Files.createDirectories(out.getParent());
            Files.copy(in, out, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
        }
    }

    TreeSet<Path> getAllRelativeFiles(Path dir) throws IOException {
        return Files.walk(dir).map(dir::relativize).collect(Collectors.toCollection(TreeSet::new));
    }

    // ----------------------------------------------------------------------
    // Protected methods
    // ----------------------------------------------------------------------

    /**
     * @return the wanted classifier, i.e. <code>javadoc</code> or <code>test-javadoc</code>
     */
    protected String getClassifier() {
        return classifier;
    }

    // ----------------------------------------------------------------------
    // private methods
    // ----------------------------------------------------------------------

    /**
     * Method that creates the jar file
     *
     * @param  javadocFiles      the directory where the generated jar file will be put
     * @param  jarFileName       the filename of the generated jar file
     * @return                   a File object that contains the generated jar file
     * @throws ArchiverException {@link ArchiverException}
     * @throws IOException       {@link IOException}
     */
    private File generateArchive(File javadocFiles, String jarFileName) throws ArchiverException, IOException {
        File javadocJar = new File(jarOutputDirectory, jarFileName);

        MavenArchiver archiver = new MavenArchiver();
        archiver.setArchiver(jarArchiver);
        archiver.setOutputFile(javadocJar);

        File contentDirectory = javadocFiles;
        if (!contentDirectory.exists()) {
            getLog().warn("JAR will be empty - no content was marked for inclusion!");
        } else {
            archiver.getArchiver().addDirectory(contentDirectory, DEFAULT_INCLUDES, DEFAULT_EXCLUDES);
        }

        List<Resource> resources = project.getBuild().getResources();

        for (Resource r : resources) {
            if (r.getDirectory().endsWith("maven-shared-archive-resources")) {
                archiver.getArchiver().addDirectory(new File(r.getDirectory()));
            }
        }

        if (useDefaultManifestFile && defaultManifestFile.exists() && archive.getManifestFile() == null) {
            getLog().info("Adding existing MANIFEST to archive. Found under: " + defaultManifestFile.getPath());
            archive.setManifestFile(defaultManifestFile);
        }

        if (archive.getManifestFile() == null) {
            try {

                Manifest manifest = archiver.getManifest(session, project, archive);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                manifest.write(baos);
                Path man = javadocFiles.toPath().resolve("META-INF/MANIFEST.MF");
                byte[] data = null;
                if (Files.isRegularFile(man)) {
                    data = Files.readAllBytes(man);
                }
                if (!Arrays.equals(data, baos.toByteArray())) {
                    Files.createDirectories(man.getParent());
                    Files.copy(new ByteArrayInputStream(baos.toByteArray()), man, StandardCopyOption.REPLACE_EXISTING);
                }
                archive.setManifestFile(man.toFile());
            } catch (ManifestException e) {
                throw new ArchiverException("ManifestException: " + e.getMessage(), e);
            } catch (DependencyResolutionRequiredException e) {
                throw new ArchiverException("DependencyResolutionRequiredException: " + e.getMessage(), e);
            }
        }

        try {
            archiver.createArchive(session, project, archive);
        } catch (ManifestException e) {
            throw new ArchiverException("ManifestException: " + e.getMessage(), e);
        } catch (DependencyResolutionRequiredException e) {
            throw new ArchiverException("DependencyResolutionRequiredException: " + e.getMessage(), e);
        }

        return javadocJar;
    }
}
