/**
 *
 * 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.karaf.tooling.features;

import static java.lang.String.format;
import static org.apache.karaf.deployer.kar.KarArtifactInstaller.FEATURE_CLASSIFIER;

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.StringWriter;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;

import javax.xml.bind.JAXBException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;

import org.apache.karaf.features.internal.model.Bundle;
import org.apache.karaf.features.internal.model.ConfigFile;
import org.apache.karaf.features.internal.model.Dependency;
import org.apache.karaf.features.internal.model.Feature;
import org.apache.karaf.features.internal.model.Features;
import org.apache.karaf.features.internal.model.JaxbUtil;
import org.apache.karaf.features.internal.model.ObjectFactory;
import org.apache.karaf.tooling.utils.DependencyHelper;
import org.apache.karaf.tooling.utils.DependencyHelperFactory;
import org.apache.karaf.tooling.utils.LocalDependency;
import org.apache.karaf.tooling.utils.ManifestUtils;
import org.apache.karaf.tooling.utils.MavenUtil;
import org.apache.karaf.tooling.utils.MojoSupport;
import org.apache.karaf.tooling.utils.SimpleLRUCache;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugin.logging.SystemStreamLog;
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.DefaultProjectBuildingRequest;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.repository.RepositorySystem;
import org.apache.maven.shared.filtering.MavenFileFilter;
import org.apache.maven.shared.filtering.MavenFilteringException;
import org.apache.maven.shared.filtering.MavenResourcesExecution;
import org.apache.maven.shared.filtering.MavenResourcesFiltering;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.xml.sax.SAXException;

/**
 * Generates the features XML file starting with an optional source feature.xml and adding
 * project dependencies as bundles and feature/car dependencies.
 *
 * NB this requires a recent maven-install-plugin such as 2.3.1
 */
@Mojo(name = "features-generate-descriptor", defaultPhase = LifecyclePhase.COMPILE, requiresDependencyResolution = ResolutionScope.RUNTIME, threadSafe = true)
public class GenerateDescriptorMojo extends MojoSupport {

    /**
     * An (optional) input feature file to extend. The plugin reads this file, and uses it as a template
     * to create the output.
     * This is highly recommended as it is the only way to add <code>&lt;feature/&gt;</code>
     * elements to the individual features that are generated.  Note that this file is filtered using standard Maven
     * resource interpolation, allowing attributes of the input file to be set with information such as ${project.version}
     * from the current build.
     * <p/>
     * When dependencies are processed, if they are duplicated in this file, the dependency here provides the baseline
     * information and is supplemented by additional information from the dependency.
     */
    @Parameter(defaultValue = "${project.basedir}/src/main/feature/feature.xml")
    private File inputFile;

    /**
     * (wrapper) The filtered input file. This file holds the result of Maven resource interpolation and is generally
     * not necessary to change, although it may be helpful for debugging.
     */
    @Parameter(defaultValue = "${project.build.directory}/feature/filteredInputFeature.xml")
    private File filteredInputFile;

    /**
     * The file to generate.  This file is attached as a project output artifact with the classifier specified by
     * <code>attachmentArtifactClassifier</code>.
     */
    @Parameter(defaultValue = "${project.build.directory}/feature/feature.xml")
    private File outputFile;

    /**
     * Exclude some artifacts from the generated feature.
     * See addBundlesToPrimaryFeature for more details.
     *
     */
    @Parameter
    private List<String> excludedArtifactIds = new ArrayList<>();

    /**
     * The resolver to use for the feature.  Normally null or "OBR" or "(OBR)"
     */
    @Parameter(defaultValue = "${resolver}")
    private String resolver;

    /**
     * The artifact type for attaching the generated file to the project
     */
    @Parameter(defaultValue = "xml")
    private String attachmentArtifactType = "xml";

    /**
     * (wrapper) The artifact classifier for attaching the generated file to the project
     */
    @Parameter(defaultValue = "features")
    private String attachmentArtifactClassifier = "features";

    /**
     * Specifies whether features dependencies of this project will be included inline in the
     * final output (<code>true</code>), or simply referenced as output artifact dependencies (<code>false</code>).
     * If <code>true</code>, feature dependencies' xml descriptors are read and their contents added to the features descriptor under assembly.
     * If <code>false</code>, feature dependencies are added to the assembled feature as dependencies.
     * Setting this value to <code>true</code> is especially helpful in multiproject builds where subprojects build their own features
     * using <code>aggregateFeatures = false</code>, then combined with <code>aggregateFeatures = true</code> in an
     * aggregation project with explicit dependencies to the child projects.
     */
    @Parameter(defaultValue = "false")
    private boolean aggregateFeatures = false;

    /**
     * If present, the bundles added to the feature constructed from the dependencies will be marked with this default
     * startlevel.  If this parameter is not present, no startlevel attribute will be created. Finer resolution for specific
     * dependencies can be obtained by specifying the dependency in the file referenced by the <code>inputFile</code> parameter.
     */
    @Parameter
    private Integer startLevel;

    /**
     * Installation mode. If present, generate "feature.install" attribute:
     * <p/>
     * <a href="http://karaf.apache.org/xmlns/features/v1.1.0">Installation mode</a>
     * <p/>
     * Can be either manual or auto. Specifies whether the feature should be automatically installed when
     * dropped inside the deploy folder. Note: this attribute doesn't affect feature descriptors that are installed
     * from the feature:install command or as part of the etc/org.apache.karaf.features.cfg file.
     */
    @Parameter
    private String installMode;

    /**
     * Flag indicating whether transitive dependencies should be included (<code>true</code>) or not (<code>false</code>).
     * <p/>
     * N.B. Note the default value of this is true, but is suboptimal in cases where specific <code>&lt;feature/&gt;</code> dependencies are
     * provided by the <code>inputFile</code> parameter.
     */
    @Parameter(defaultValue = "true")
    private boolean includeTransitiveDependency;

    /**
     * The standard behavior is to add dependencies as <code>&lt;bundle&gt;</code> elements to a <code>&lt;feature&gt;</code>
     * with the same name as the artifactId of the project.  This flag disables that behavior.
     * If this parameter is <code>true</code>, then two other parameters refine the list of bundles added to the primary feature:
     * <code>excludedArtifactIds</code> and <code>ignoreScopeProvided</code>. Each of these specifies dependent artifacts
     * that should <strong>not</strong> be added to the primary feature.
     * <p>
     *     Note that you may tune the <code>bundle</code> elements by including them in the <code>inputFile</code>.
     *     If the <code>inputFile</code> has a <code>feature</code> element for the primary feature, the plugin will
     *     respect it, so that you can, for example, set the <code>startLevel</code> or <code>start</code> attribute.
     * </p>
     *
     */
    @Parameter(defaultValue = "true")
    private boolean addBundlesToPrimaryFeature;

    /**
     * The standard behavior is to add any dependencies other than those in the <code>runtime</code> scope to the feature bundle.
     * Setting this flag to "true" disables adding any dependencies (transient or otherwise) that are in
     * <code>&lt;scope&gt;provided&lt;/scope&gt;</code>. See <code>addBundlesToPrimaryFeature</code> for more details.
     */
    @Parameter(defaultValue = "false")
    private boolean ignoreScopeProvided;

    /**
     * Flag indicating whether the main project artifact should be included (<code>true</code>) or not (<code>false</code>).
     * This parameter is useful when you add an execution of this plugin to a project with some packaging that is <strong>not</strong>
     * <code>feature</code>. If you don't set this, then you will get a feature that contains the dependencies but
     * not the primary artifact itself.
     * <p/>
     * Assumes the main project artifact is a bundle and the feature will be attached alongside using <code>attachmentArtifactClassifier</code>.
     */
    @Parameter(defaultValue = "false")
    private boolean includeProjectArtifact;

    /**
     * The name of the primary feature. This is the feature that will be created or modified to include the
     * main project artifact and/or the bundles.
     * @see #addBundlesToPrimaryFeature
     * @see #includeProjectArtifact
     */
    @Parameter(defaultValue = "${project.artifactId}")
    private String primaryFeatureName;

    /**
     * Flag indicating whether bundles should use the version range declared in the POM. If <code>false</code>,
     * the actual version of the resolved artifacts will be used.
     */
    @Parameter(defaultValue = "false")
    private boolean useVersionRange;

    /**
     * Flag indicating whether the plugin should determine whether transitive dependencies are declared with
     * a version range. If this flag is set to <code>true</code> and a transitive dependency has been found
     * which had been declared with a version range, that version range will be used to build the appropriate
     * bundle element instead of the newest version. This flag has only an effect when {@link #useVersionRange}
     * is <code>true</code>
     */
    @Parameter(defaultValue = "false")
    private boolean includeTransitiveVersionRanges;

    @Parameter
    private Boolean enableGeneration;

    /**
     * Flag indicating whether the plugin should simplify bundle dependencies. If the flag is set to {@code true}
     * and a bundle dependency is determined to be included in a feature dependency, the bundle dependency is
     * dropped.
     */
    @Parameter(defaultValue = "false")
    private boolean simplifyBundleDependencies;

    /**
     * Maximum size of the artifact LRU cache. This cache is used to prevent repeated artifact-to-file resolution.
     */
    @Parameter(defaultValue = "1024")
    private int artifactCacheSize;

    /**
     * Maximum size of the Features LRU cache. This cache is used to prevent repeated deserialization of features
     * XML files.
     */
    @Parameter(defaultValue = "256")
    private int featuresCacheSize;

    /**
     * Name of features which are prerequisites (they still need to be defined separately).
     */
    @Parameter
    private List<String> prerequisiteFeatures = new ArrayList<>();

    /**
     * Name of features which are dependencies (they still need to be defined separately).
     */
    @Parameter
    private List<String> dependencyFeatures = new ArrayList<>();

    // *************************************************
    // READ-ONLY MAVEN PLUGIN PARAMETERS
    // *************************************************

    /**
     * We can't autowire strongly typed RepositorySystem from Aether because it may be Sonatype (Maven 3.0.x)
     * or Eclipse (Maven 3.1.x/3.2.x) implementation, so we switch to service locator.
     */
    @Component
    private PlexusContainer container;

    @Component
    private RepositorySystem repoSystem;

    @Component
    protected MavenResourcesFiltering mavenResourcesFiltering;

    @Component
    protected MavenFileFilter mavenFileFilter;

	@Component
	private ProjectBuilder mavenProjectBuilder;

    // dependencies we are interested in
    protected Collection<LocalDependency> localDependencies;

    // log of what happened during search
    protected String treeListing;

    // an access layer for available Aether implementation
    protected DependencyHelper dependencyHelper;

    // maven log
    private Log log;

    // If useVersionRange is true, this map will be used to cache
    // resolved MavenProjects
    private final Map<Artifact, MavenProject> resolvedProjects = new HashMap<>();

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        try {
            if (enableGeneration == null) {
                String packaging = this.project.getPackaging();
                enableGeneration = !"feature".equals(packaging);
            }

            if (!enableGeneration) {
                if (inputFile.exists()) {
                    File dir = outputFile.getParentFile();
                    if (!dir.isDirectory() && !dir.mkdirs()) {
                        throw new MojoExecutionException("Could not create directory for features file: " + dir);
                    }
                    filter(inputFile, outputFile);
                    projectHelper.attachArtifact(project, attachmentArtifactType, attachmentArtifactClassifier, outputFile);
                    return;
                }
            }

            this.dependencyHelper = DependencyHelperFactory.createDependencyHelper(this.container, this.project,
                this.mavenSession, this.artifactCacheSize, getLog());
            this.dependencyHelper.getDependencies(project, includeTransitiveDependency);
            this.localDependencies = dependencyHelper.getLocalDependencies();
            this.treeListing = dependencyHelper.getTreeListing();
            File dir = outputFile.getParentFile();
            if (dir.isDirectory() || dir.mkdirs()) {
                try (PrintStream out = new PrintStream(new FileOutputStream(outputFile))) {
                    writeFeatures(out);
                }
                // now lets attach it
                projectHelper.attachArtifact(project, attachmentArtifactType, attachmentArtifactClassifier, outputFile);
            } else {
                throw new MojoExecutionException("Could not create directory for features file: " + dir);
            }
        } catch (Exception e) {
            getLog().error(e.getMessage());
            throw new MojoExecutionException("Unable to create features.xml file: " + e, e);
        }
    }

	private MavenProject resolveProject(final Object artifact) throws MojoExecutionException {
		MavenProject resolvedProject = project;
		if (includeTransitiveVersionRanges) {
			resolvedProject = resolvedProjects.get(artifact);
			if (resolvedProject == null) {
				final ProjectBuildingRequest request = new DefaultProjectBuildingRequest();

				// Fixes KARAF-4626; if the system properties are not transferred to the request,
				// test-feature-use-version-range-transfer-properties will fail
				request.setSystemProperties(System.getProperties());

				request.setResolveDependencies(true);
				request.setRemoteRepositories(project.getPluginArtifactRepositories());
				request.setLocalRepository(localRepo);
				request.setProfiles(new ArrayList<>(mavenSession.getRequest().getProfiles()));
				request.setActiveProfileIds(new ArrayList<>(mavenSession.getRequest().getActiveProfiles()));
				dependencyHelper.setRepositorySession(request);
				final Artifact pomArtifact = repoSystem.createArtifact(dependencyHelper.getGroupId(artifact),
						dependencyHelper.getArtifactId(artifact), dependencyHelper.getBaseVersion(artifact), "pom");
				try {
					resolvedProject = mavenProjectBuilder.build(pomArtifact, request).getProject();
					resolvedProjects.put(pomArtifact, resolvedProject);
				} catch (final ProjectBuildingException e) {
					throw new MojoExecutionException(
							format("Maven-project could not be built for artifact %s", pomArtifact), e);
				}
			}
		}
		return resolvedProject;
	}

	private String getVersionOrRange(final Object parent, final Object artifact) throws MojoExecutionException {
		String versionOrRange = dependencyHelper.getBaseVersion(artifact);
		if (useVersionRange) {
			for (final org.apache.maven.model.Dependency dependency : resolveProject(parent).getDependencies()) {

				if (dependency.getGroupId().equals(dependencyHelper.getGroupId(artifact))
						&& dependency.getArtifactId().equals(dependencyHelper.getArtifactId(artifact))) {
					versionOrRange = dependency.getVersion();
					break;
				}
			}
		}
		return versionOrRange;
	}

    /*
     * Write all project dependencies as feature
     */
    private void writeFeatures(PrintStream out) throws ArtifactResolutionException, ArtifactNotFoundException,
            IOException, JAXBException, SAXException, ParserConfigurationException, XMLStreamException, MojoExecutionException {
        getLog().info("Generating feature descriptor file " + outputFile.getAbsolutePath());
        //read in an existing feature.xml
        ObjectFactory objectFactory = new ObjectFactory();
        Features features;
        if (inputFile.exists()) {
            filter(inputFile, filteredInputFile);
            features = readFeaturesFile(filteredInputFile);
        } else {
            features = objectFactory.createFeaturesRoot();
        }
        if (features.getName() == null) {
            features.setName(project.getArtifactId());
        }

        Feature feature = null;
        for (Feature test : features.getFeature()) {
            if (test.getName().equals(primaryFeatureName)) {
                feature = test;
            }
        }
        if (feature == null) {
            feature = objectFactory.createFeature();
            feature.setName(primaryFeatureName);
        }
        if (!feature.hasVersion()) {
            feature.setVersion(project.getArtifact().getBaseVersion());
        }
        if (feature.getDescription() == null) {
            feature.setDescription(project.getName());
        }
        if (installMode != null) {
            feature.setInstall(installMode);
        }
        if (project.getDescription() != null && feature.getDetails() == null) {
            feature.setDetails(project.getDescription());
        }
        if (includeProjectArtifact) {
            Bundle bundle = objectFactory.createBundle();
            bundle.setLocation(this.dependencyHelper.artifactToMvn(project.getArtifact(), project.getVersion()));
            if (startLevel != null) {
                bundle.setStartLevel(startLevel);
            }
            feature.getBundle().add(bundle);
        }
        boolean needWrap = false;

        // First pass to look for features
        // Track other features we depend on and their repositories (we track repositories instead of building them from
        // the feature's Maven artifact to allow for multi-feature repositories)
        // TODO Initialise the repositories from the existing feature file if any
        Map<Dependency, Feature> otherFeatures = new HashMap<>();
        Map<Feature, String> featureRepositories = new HashMap<>();
        FeaturesCache cache = new FeaturesCache(featuresCacheSize, artifactCacheSize);
        for (final LocalDependency entry : localDependencies) {
            Object artifact = entry.getArtifact();

            if (excludedArtifactIds.contains(this.dependencyHelper.getArtifactId(artifact))) {
                continue;
            }

            processFeatureArtifact(features, feature, otherFeatures, featureRepositories, cache, artifact,
                    entry.getParent(), true);
        }
        // Do not retain cache beyond this point
        cache = null;

        // Second pass to look for bundles
        if (addBundlesToPrimaryFeature) {
            localDependency:
            for (final LocalDependency entry : localDependencies) {
                Object artifact = entry.getArtifact();

                if (excludedArtifactIds.contains(this.dependencyHelper.getArtifactId(artifact))) {
                    continue;
                }

                if (!this.dependencyHelper.isArtifactAFeature(artifact)) {
                    String bundleName = this.dependencyHelper.artifactToMvn(artifact, getVersionOrRange(entry.getParent(), artifact));

                    for (ConfigFile cf : feature.getConfigfile()) {
                        if (bundleName.equals(cf.getLocation().replace('\n', ' ').trim())) {
                            // The bundle matches a configfile, ignore it
                            continue localDependency;
                        }
                    }

                    File bundleFile = this.dependencyHelper.resolve(artifact, getLog());
                    Manifest manifest = getManifest(bundleFile);
                    if (manifest == null || !ManifestUtils.isBundle(manifest)) {
                        bundleName = "wrap:" + bundleName;
                        needWrap = true;
                    }

                    Bundle bundle = null;
                    for (Bundle b : feature.getBundle()) {
                        if (bundleName.equals(b.getLocation())) {
                            bundle = b;
                            break;
                        }
                    }
                    if (bundle == null) {
                        bundle = objectFactory.createBundle();
                        bundle.setLocation(bundleName);
                        // Check the features this feature depends on don't already contain the dependency
                        // TODO Perhaps only for transitive dependencies?
                        boolean includedTransitively =
                            simplifyBundleDependencies && isBundleIncludedTransitively(feature, otherFeatures, bundle);
                        if (!includedTransitively && (!"provided".equals(entry.getScope()) || !ignoreScopeProvided)) {
                            feature.getBundle().add(bundle);
                        }
                    }
                    if ("runtime".equals(entry.getScope())) {
                        bundle.setDependency(true);
                    }
                    if (startLevel != null && bundle.getStartLevel() == 0) {
                        bundle.setStartLevel(startLevel);
                    }
                }
            }
        }

        if (needWrap) {
            Dependency wrapDependency = new Dependency();
            wrapDependency.setName("wrap");
            wrapDependency.setDependency(false);
            wrapDependency.setPrerequisite(true);
            feature.getFeature().add(wrapDependency);
        }

        if ((!feature.getBundle().isEmpty() || !feature.getFeature().isEmpty()) && !features.getFeature().contains(feature)) {
            features.getFeature().add(feature);
        }

        // Add any missing repositories for the included features
        for (Feature includedFeature : features.getFeature()) {
            for (Dependency dependency : includedFeature.getFeature()) {
                Feature dependedFeature = otherFeatures.get(dependency);
                if (dependedFeature != null && !features.getFeature().contains(dependedFeature)) {
                    String repository = featureRepositories.get(dependedFeature);
                    if (repository != null && !features.getRepository().contains(repository)) {
                        features.getRepository().add(repository);
                    }
                }
            }
        }

        JaxbUtil.marshal(features, out);
        try {
            checkChanges(features, objectFactory);
        } catch (Exception e) {
            throw new MojoExecutionException("Features contents have changed", e);
        }
        getLog().info("...done!");
    }

    private void processFeatureArtifact(Features features, Feature feature, Map<Dependency, Feature> otherFeatures,
                                        Map<Feature, String> featureRepositories, FeaturesCache cache,
                                        Object artifact, Object parent, boolean add)
            throws MojoExecutionException, XMLStreamException, JAXBException, IOException {
        if (this.dependencyHelper.isArtifactAFeature(artifact) && FEATURE_CLASSIFIER.equals(
                this.dependencyHelper.getClassifier(artifact))) {
            File featuresFile = this.dependencyHelper.resolve(artifact, getLog());
            if (featuresFile == null || !featuresFile.exists()) {
                throw new MojoExecutionException(
                        "Cannot locate file for feature: " + artifact + " at " + featuresFile);
            }
            Features includedFeatures = cache.getFeature(featuresFile);
            for (String repository : includedFeatures.getRepository()) {
                processFeatureArtifact(features, feature, otherFeatures, featureRepositories, cache,
                        cache.getArtifact(repository), parent, false);
            }
            for (Feature includedFeature : includedFeatures.getFeature()) {
                Dependency dependency = new Dependency(includedFeature.getName(), includedFeature.getVersion());
                dependency.setPrerequisite(prerequisiteFeatures.contains(dependency.getName()));
                dependency.setDependency(dependencyFeatures.contains(dependency.getName()));
                // Determine what dependency we're actually going to use
                Dependency matchingDependency = findMatchingDependency(feature.getFeature(), dependency);
                if (matchingDependency != null) {
                    // The feature already has a matching dependency, merge
                    mergeDependencies(matchingDependency, dependency);
                    dependency = matchingDependency;
                }
                // We mustn't de-duplicate here, we may have seen a feature in !add mode
                otherFeatures.put(dependency, includedFeature);
                if (add) {
                    if (!feature.getFeature().contains(dependency)) {
                        feature.getFeature().add(dependency);
                    }
                    if (aggregateFeatures) {
                        features.getFeature().add(includedFeature);
                    }
                }
                if (!featureRepositories.containsKey(includedFeature)) {
                    featureRepositories.put(includedFeature,
                            this.dependencyHelper.artifactToMvn(artifact, getVersionOrRange(parent, artifact)));
                }
            }
        }
    }

    private static Dependency findMatchingDependency(Collection<Dependency> dependencies, Dependency reference) {
        String referenceName = reference.getName();
        for (Dependency dependency : dependencies) {
            if (referenceName.equals(dependency.getName())) {
                return dependency;
            }
        }
        return null;
    }

    private static void mergeDependencies(Dependency target, Dependency source) {
        if (target.getVersion() == null || Feature.DEFAULT_VERSION.equals(target.getVersion())) {
            target.setVersion(source.getVersion());
        }
        if (source.isDependency()) {
            target.setDependency(true);
        }
        if (source.isPrerequisite()) {
            target.setPrerequisite(true);
        }
    }

    private boolean isBundleIncludedTransitively(Feature feature, Map<Dependency, Feature> otherFeatures,
                                                 Bundle bundle) {
        for (Dependency dependency : feature.getFeature()) {
            // Match dependencies “generously” (we might be matching single-version dependencies with version ranges)
            Dependency otherDependency = findMatchingDependency(otherFeatures.keySet(), dependency);
            Feature otherFeature = otherDependency != null ? otherFeatures.get(otherDependency) : null;
            if (otherFeature != null) {
                if (otherFeature.getBundle().contains(bundle) || isBundleIncludedTransitively(otherFeature,
                    otherFeatures, bundle)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Extract the MANIFEST from the give file.
     */

    private Manifest getManifest(File file) throws IOException {
        final InputStream is;
        try {
            is = Files.newInputStream(file.toPath());
        } catch (Exception e) {
            getLog().warn("Error while opening artifact", e);
            return null;
        }

        try (BufferedInputStream bis = new BufferedInputStream(is)) {
            bis.mark(256 * 1024);

            try (JarInputStream jar = new JarInputStream(bis)) {
                Manifest m = jar.getManifest();
                if (m == null) {
                    getLog().warn("Manifest not present in the first entry of the zip - " + file.getName());
                }
                return m;
            }
        }
    }

    static Features readFeaturesFile(File featuresFile) throws XMLStreamException, JAXBException, IOException {
        return JaxbUtil.unmarshal(featuresFile.toURI().toASCIIString(), false);
    }

    @Override
    public void setLog(Log log) {
        this.log = log;
    }

    @Override
    public Log getLog() {
        if (log == null) {
            setLog(new SystemStreamLog());
        }
        return log;
    }

    //------------------------------------------------------------------------//
    // dependency change detection

    /**
     * Master switch to look for and log changed dependencies.  If this is set to <code>true</code> and the file referenced by
     * <code>dependencyCache</code> does not exist, it will be unconditionally generated.  If the file does exist, it is
     * used to detect changes from previous builds and generate logs of those changes.  In that case,
     * <code>failOnDependencyChange = true</code> will cause the build to fail.
     */
    @Parameter(defaultValue = "false")
    private boolean checkDependencyChange;

    /**
     * (wrapper) Location of dependency cache.  This file is generated to contain known dependencies and is generally
     * located in SCM so that it may be used across separate developer builds. This is parameter is ignored unless
     * <code>checkDependencyChange</code> is set to <code>true</code>.
     */
    @Parameter(defaultValue = "${basedir}/src/main/history/dependencies.xml")
    private File dependencyCache;

    /**
     * Location of filtered dependency file.
     */
    @Parameter(defaultValue = "${basedir}/target/history/dependencies.xml", readonly = true)
    private File filteredDependencyCache;

    /**
     * Whether to fail on changed dependencies (default, <code>true</code>) or warn (<code>false</code>). This is parameter is ignored unless
     * <code>checkDependencyChange</code> is set to <code>true</code> and <code>dependencyCache</code> exists to compare
     * against.
     */
    @Parameter(defaultValue = "true")
    private boolean failOnDependencyChange;

    /**
     * Copies the contents of dependency change logs that are generated to stdout. This is parameter is ignored unless
     * <code>checkDependencyChange</code> is set to <code>true</code> and <code>dependencyCache</code> exists to compare
     * against.
     */
    @Parameter(defaultValue = "false")
    private boolean logDependencyChanges;

    /**
     * Whether to overwrite the file referenced by <code>dependencyCache</code> if it has changed.  This is parameter is
     * ignored unless <code>checkDependencyChange</code> is set to <code>true</code>, <code>failOnDependencyChange</code>
     * is set to <code>false</code> and <code>dependencyCache</code> exists to compare against.
     */
    @Parameter(defaultValue = "false")
    private boolean overwriteChangedDependencies;

    //filtering support
    /**
     * The character encoding scheme to be applied when filtering resources.
     */
    @Parameter(defaultValue = "${project.build.sourceEncoding}")
    protected String encoding;

    /**
     * Expression preceded with the String won't be interpolated
     * \${foo} will be replaced with ${foo}
     */
    @Parameter(defaultValue = "${maven.resources.escapeString}")
    protected String escapeString = "\\";

    /**
     * System properties.
     */
    @Parameter
    protected Map<String, String> systemProperties;

    private void checkChanges(Features newFeatures, ObjectFactory objectFactory) throws Exception {
        if (checkDependencyChange) {
            //combine all the dependencies to one feature and strip out versions
            Features features = objectFactory.createFeaturesRoot();
            features.setName(newFeatures.getName());
            Feature feature = objectFactory.createFeature();
            features.getFeature().add(feature);
            for (Feature f : newFeatures.getFeature()) {
                for (Bundle b : f.getBundle()) {
                    Bundle bundle = objectFactory.createBundle();
                    bundle.setLocation(b.getLocation());
                    feature.getBundle().add(bundle);
                }
                for (Dependency d : f.getFeature()) {
                    Dependency dependency = objectFactory.createDependency();
                    dependency.setName(d.getName());
                    feature.getFeature().add(dependency);
                }
            }

            feature.getBundle().sort(Comparator.comparing(Bundle::getLocation));
            feature.getFeature().sort(Comparator.comparing(Dependency::getName));

            if (dependencyCache.exists()) {
                //filter dependencies file
                filter(dependencyCache, filteredDependencyCache);
                //read dependency types, convert to dependencies, compare.
                Features oldfeatures = readFeaturesFile(filteredDependencyCache);
                Feature oldFeature = oldfeatures.getFeature().get(0);

                List<Bundle> addedBundles = new ArrayList<>(feature.getBundle());
                List<Bundle> removedBundles = new ArrayList<>();
                for (Bundle test : oldFeature.getBundle()) {
                    boolean t1 = addedBundles.contains(test);
                    int s1 = addedBundles.size();
                    boolean t2 = addedBundles.remove(test);
                    int s2 = addedBundles.size();
                    if (t1 != t2) {
                        getLog().warn("dependencies.contains: " + t1 + ", dependencies.remove(test): " + t2);
                    }
                    if (t1 == (s1 == s2)) {
                        getLog().warn("dependencies.contains: " + t1 + ", size before: " + s1 + ", size after: " + s2);
                    }
                    if (!t2) {
                        removedBundles.add(test);
                    }
                }

                List<Dependency> addedDependencys = new ArrayList<>(feature.getFeature());
                List<Dependency> removedDependencys = new ArrayList<>();
                for (Dependency test : oldFeature.getFeature()) {
                    boolean t1 = addedDependencys.contains(test);
                    int s1 = addedDependencys.size();
                    boolean t2 = addedDependencys.remove(test);
                    int s2 = addedDependencys.size();
                    if (t1 != t2) {
                        getLog().warn("dependencies.contains: " + t1 + ", dependencies.remove(test): " + t2);
                    }
                    if (t1 == (s1 == s2)) {
                        getLog().warn("dependencies.contains: " + t1 + ", size before: " + s1 + ", size after: " + s2);
                    }
                    if (!t2) {
                        removedDependencys.add(test);
                    }
                }
                if (!addedBundles.isEmpty() || !removedBundles.isEmpty() || !addedDependencys.isEmpty() || !removedDependencys.isEmpty()) {
                    saveDependencyChanges(addedBundles, removedBundles, addedDependencys, removedDependencys, objectFactory);
                    if (overwriteChangedDependencies) {
                        writeDependencies(features, dependencyCache);
                    }
                } else {
                    getLog().info(saveTreeListing());
                }

            } else {
                writeDependencies(features, dependencyCache);
            }
        }
    }

    protected void saveDependencyChanges(Collection<Bundle> addedBundles, Collection<Bundle> removedBundles, Collection<Dependency> addedDependencys, Collection<Dependency> removedDependencys, ObjectFactory objectFactory)
            throws Exception {
        File addedFile = new File(filteredDependencyCache.getParentFile(), "dependencies.added.xml");
        Features added = toFeatures(addedBundles, addedDependencys, objectFactory);
        writeDependencies(added, addedFile);

        File removedFile = new File(filteredDependencyCache.getParentFile(), "dependencies.removed.xml");
        Features removed = toFeatures(removedBundles, removedDependencys, objectFactory);
        writeDependencies(removed, removedFile);

        StringWriter out = new StringWriter();
        out.write(saveTreeListing());

        out.write("Dependencies have changed:\n");
        if (!addedBundles.isEmpty() || !addedDependencys.isEmpty()) {
            out.write("\tAdded dependencies are saved here: " + addedFile.getAbsolutePath() + "\n");
            if (logDependencyChanges) {
                JaxbUtil.marshal(added, out);
            }
        }
        if (!removedBundles.isEmpty() || !removedDependencys.isEmpty()) {
            out.write("\tRemoved dependencies are saved here: " + removedFile.getAbsolutePath() + "\n");
            if (logDependencyChanges) {
                JaxbUtil.marshal(removed, out);
            }
        }
        out.write("Delete " + dependencyCache.getAbsolutePath()
                + " if you are happy with the dependency changes.");

        if (failOnDependencyChange) {
            throw new MojoFailureException(out.toString());
        } else {
            getLog().warn(out.toString());
        }
    }

    private static Features toFeatures(Collection<Bundle> addedBundles, Collection<Dependency> addedDependencys, ObjectFactory objectFactory) {
        Features features = objectFactory.createFeaturesRoot();
        Feature feature = objectFactory.createFeature();
        feature.getBundle().addAll(addedBundles);
        feature.getFeature().addAll(addedDependencys);
        features.getFeature().add(feature);
        return features;
    }


    private static void writeDependencies(Features features, File file) throws JAXBException, IOException {
        file.getParentFile().mkdirs();
        if (!file.getParentFile().exists() || !file.getParentFile().isDirectory()) {
            throw new IOException("Cannot create directory at " + file.getParent());
        }
        try (OutputStream out = new FileOutputStream(file)) {
            JaxbUtil.marshal(features, out);
        }
    }

    protected void filter(File sourceFile, File targetFile)
            throws MojoExecutionException {
        try {

            if (StringUtils.isEmpty(encoding)) {
                getLog().warn(
                        "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
                                + ", i.e. build is platform dependent!");
            }
            targetFile.getParentFile().mkdirs();

            final MavenResourcesExecution mre = new MavenResourcesExecution();
            mre.setMavenProject(project);
            mre.setMavenSession(mavenSession);
            mre.setFilters(null);
            mre.setEscapedBackslashesInFilePath(true);
            final LinkedHashSet<String> delimiters = new LinkedHashSet<>();
            delimiters.add("${*}");
            mre.setDelimiters(delimiters);

            @SuppressWarnings("rawtypes")
            List filters = mavenFileFilter.getDefaultFilterWrappers(mre);
            mavenFileFilter.copyFile(sourceFile, targetFile, true, filters, encoding, true);
        } catch (MavenFilteringException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        }
    }

    protected String saveTreeListing() throws IOException {
        File treeListFile = new File(filteredDependencyCache.getParentFile(), "treeListing.txt");
        try (OutputStream os = new FileOutputStream(treeListFile);
             BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os))) {
            writer.write(treeListing);
        }
        return "\tTree listing is saved here: " + treeListFile.getAbsolutePath() + "\n";
    }

    private static final class FeaturesCache {
        // Maven-to-Aether Artifact cache, as parsing strings is expensive
        private final SimpleLRUCache<String, DefaultArtifact> artifactCache;
        private final SimpleLRUCache<File, Features> featuresCache;

        FeaturesCache(int featuresCacheSize, int artifactCacheSize) {
            featuresCache = new SimpleLRUCache<>(featuresCacheSize);
            artifactCache = new SimpleLRUCache<>(artifactCacheSize);
        }

        DefaultArtifact getArtifact(String mavenName) {
            return artifactCache.computeIfAbsent(mavenName, MavenUtil::mvnToArtifact);
        }

        Features getFeature(final File featuresFile) throws XMLStreamException, JAXBException, IOException {
            final Features existing = featuresCache.get(featuresFile);
            if (existing != null) {
                return existing;
            }

            final Features computed = readFeaturesFile(featuresFile);
            featuresCache.put(featuresFile, computed);
            return computed;
        }
    }
}
