/*
 *  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.synapse.maven.xar;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactCollector;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.DebugResolutionListener;
import org.apache.maven.artifact.resolver.ResolutionListener;
import org.apache.maven.artifact.resolver.filter.AndArtifactFilter;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
import org.apache.maven.artifact.resolver.filter.TypeArtifactFilter;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.artifact.InvalidDependencyVersionException;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.logging.LogEnabled;
import org.codehaus.plexus.logging.Logger;

/**
 * Abstract base class for all the mojos in the XAR plugin.
 */
public abstract class AbstractXarMojo extends AbstractMojo implements LogEnabled {
    /**
     * List of dependencies to be excluded by default because the corresponding APIs are provided
     * by the Synapse runtime.
     */
    private static final String[] defaultRuntimeExcludes = {
        "org.apache.synapse:synapse-core:jar",
        "commons-logging:commons-logging-api:jar",
    }; 
    
    private static final String[] serviceClassNames = {
        "org.apache.synapse.config.xml.MediatorFactory",
        "org.apache.synapse.config.xml.MediatorSerializer",
        "org.apache.synapse.config.xml.StartupFactory",
    };
    
    /**
     * The projects base directory.
     *
     * @parameter expression="${project.basedir}"
     * @required
     * @readonly
     */
    private File baseDir;

    /**
     * The maven project.
     *
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    protected MavenProject project;

    /**
     * Local maven repository.
     * 
     * @parameter expression="${localRepository}"
     * @required
     * @readonly
     */
    private ArtifactRepository localRepository;
    
    /**
     * Remote repositories.
     * 
     * @parameter expression="${project.remoteArtifactRepositories}"
     * @required
     * @readonly
     */
    private List remoteArtifactRepositories;
    
    /**
     * @component role="org.apache.maven.artifact.metadata.ArtifactMetadataSource" hint="maven"
     */
    private ArtifactMetadataSource artifactMetadataSource;

    /**
     * Artifact collector, needed to resolve dependencies.
     * 
     * @component role="org.apache.maven.artifact.resolver.ArtifactCollector"
     * @required
     * @readonly
     */
    private ArtifactCollector artifactCollector;
    
    /**
     * Project builder.
     * 
     * @component role="org.apache.maven.project.MavenProjectBuilder"
     * @required
     * @readonly
     */
    private MavenProjectBuilder projectBuilder;
    
    /**
     * Artifact factory.
     * 
     * @component role="org.apache.maven.artifact.factory.ArtifactFactory"
     * @required
     * @readonly
     */
    private ArtifactFactory artifactFactory;
    
    /**
     * The directory containing generated classes.
     *
     * @parameter expression="${project.build.outputDirectory}"
     * @required
     */
    private File buildOutputDirectory;

    /**
     * The directory where temporary files for inclusion in the XAR are stored.
     *
     * @parameter expression="${project.build.directory}/xar-files"
     * @required
     */
    private File tmpDirectory;

    /**
     * Whether the dependency jars should be included in the XAR.
     *
     * @parameter expression="${includeDependencies}" default-value="true"
     */
    private boolean includeDependencies;
    
    /**
     * Whether metadata for the extensions should be generated automatically.
     * 
     * @parameter expression="${generateMetadata}" default-value="true"
     */
    private boolean generateMetadata;
    
    private Logger logger;
    
    @Override
    public void enableLogging(Logger logger) {
        this.logger = logger;
    }

    /**
     * Build the XAR using the provided archiver.
     *
     * @throws MojoExecutionException
     */
    protected void buildArchive(Archiver archiver)
            throws ArchiverException, MojoExecutionException {
        
        Log log = getLog();
        log.debug("Using base directory: " + baseDir);
        archiver.addDirectory(buildOutputDirectory);
        if (includeDependencies) {
            log.debug("Adding dependencies ...");
            addDependencies(archiver);
        }
        if (generateMetadata) {
            log.debug("Generating XAR metadata ...");
            generateMetadata(archiver);
        }
    }
    
    private void addDependencies(Archiver archiver)
            throws ArchiverException, MojoExecutionException {
        
        Log log = getLog();
        AndArtifactFilter filter = new AndArtifactFilter();
        filter.add(new ScopeArtifactFilter(Artifact.SCOPE_RUNTIME));
        filter.add(new ArtifactFilter() {
            @Override
            public boolean include(Artifact artifact) {
                return !artifact.isOptional();
            }
        });
        filter.add(new TypeArtifactFilter("jar"));
        filter.add(buildSynapseRuntimeArtifactFilter());
        for (Artifact artifact : filterArtifacts(project.getArtifacts(), filter)) {
            String targetFileName = artifact.getArtifactId() + "-" + artifact.getVersion() + "." +
                    artifact.getArtifactHandler().getExtension();
            log.info("Adding " + targetFileName + " (scope " + artifact.getScope() + ")");
            archiver.addFile(artifact.getFile(), "lib/" + targetFileName);
        }
    }
    
    private void generateMetadata(Archiver archiver)
            throws ArchiverException, MojoExecutionException {
        
        Log log = getLog();
        File tmpServicesDir = new File(new File(tmpDirectory, "META-INF"), "services");
        File buildServicesDir = new File(new File(buildOutputDirectory, "META-INF"), "services");
        if (!tmpServicesDir.exists() && !tmpServicesDir.mkdirs()) {
            throw new MojoExecutionException("Error while creating the directory: " +
                    tmpServicesDir.getPath());
        }
        
        log.debug("Initializing class scanner ...");
        ClassScanner scanner = new ClassScanner(buildOutputDirectory);
        for (Artifact artifact : filterArtifacts(project.getArtifacts(),
                new ScopeArtifactFilter(Artifact.SCOPE_COMPILE))) {
            scanner.addToClasspath(artifact.getFile());
        }
        List<ServiceLocator> serviceLocators =
            new ArrayList<ServiceLocator>(serviceClassNames.length);
        for (String serviceClassName : serviceClassNames) {
            // If the user provided its own service file, skip generation
            File file = new File(buildServicesDir, serviceClassName);
            if (file.exists()) {
                log.debug(file + " exists; don't scan for " + serviceClassName +
                        " implementation");
            } else {
                ServiceLocator sl = new ServiceLocator(serviceClassName);
                serviceLocators.add(sl);
                scanner.addVisitor(sl);
            }
        }
        try {
            scanner.scan();
        } catch (ClassScannerException e) {
            throw new MojoExecutionException("Failed to scan classes for services", e);
        }
        for (ServiceLocator sl : serviceLocators) {
            File file = new File(tmpServicesDir, sl.getServiceClassName());
            if (!sl.getImplementations().isEmpty()) {
                String destFileName = "META-INF/services/" + sl.getServiceClassName();
                log.info("Generating " + destFileName);
                try {
                    Writer out = new OutputStreamWriter(new FileOutputStream(file));
                    try {
                        for (String impl : sl.getImplementations()) {
                            log.debug("  " + impl);
                            out.write(impl);
                            out.write("\n");
                        }
                    } finally {
                        out.close();
                    }
                } catch (IOException e) {
                    throw new MojoExecutionException("Unable to create temporary file " + file, e);
                }
                archiver.addFile(file, destFileName);
            }
        }
    }
    
    /**
     * Build a filter that excludes all artifacts that are provided by Synapse at runtime.
     * 
     * @return
     * @throws MojoExecutionException
     */
    private ArtifactFilter buildSynapseRuntimeArtifactFilter() throws MojoExecutionException {
        final Map<String,Artifact> artifacts = new HashMap<String,Artifact>();
        for (Artifact artifact : getSynapseRuntimeArtifacts()) {
            artifacts.put(artifact.getDependencyConflictId(), artifact);
        }
        final Set<String> defaultExclusionSet
                = new HashSet<String>(Arrays.asList(defaultRuntimeExcludes));
        return new ArtifactFilter() {
            @Override
            public boolean include(Artifact artifact) {
                Artifact runtimeArtifact = artifacts.get(artifact.getDependencyConflictId());
                if (runtimeArtifact == null) {
                    return !defaultExclusionSet.contains(artifact.getDependencyConflictId());
                } else {
                    if (!runtimeArtifact.getVersion().equals(artifact.getVersion())) {
                        getLog().warn("Possible runtime version conflict for "
                                + artifact.getArtifactId() + ": XAR depends on "
                                + artifact.getVersion() + ", Synapse runtime provides "
                                + runtimeArtifact.getVersion());
                    }
                    return false;
                }
            }
        };
    }
    
    /**
     * Get the set of artifacts that are provided by Synapse at runtime.
     * 
     * @return
     * @throws MojoExecutionException
     */
    private Set<Artifact> getSynapseRuntimeArtifacts() throws MojoExecutionException {
        Log log = getLog();
        log.debug("Looking for synapse-core artifact in XAR project dependencies ...");
        Artifact synapseCore = null;
        for (Iterator<?> it = project.getDependencyArtifacts().iterator(); it.hasNext(); ) {
            Artifact artifact = (Artifact)it.next();
            if (artifact.getGroupId().equals("org.apache.synapse")
                    && artifact.getArtifactId().equals("synapse-core")) {
                synapseCore = artifact;
                break;
            }
        }
        if (synapseCore == null) {
            throw new MojoExecutionException("Could not locate dependency on synapse-core");
        }
        
        log.debug("Loading project data for " + synapseCore + " ...");
        MavenProject synapseCoreProject;
        try {
            synapseCoreProject = projectBuilder.buildFromRepository(synapseCore,
                    remoteArtifactRepositories, localRepository);
        } catch (ProjectBuildingException e) {
            throw new MojoExecutionException("Unable to retrieve project information for "
                    + synapseCore, e);
        }
        Set<Artifact> synapseRuntimeDeps;
        try {
            synapseRuntimeDeps = synapseCoreProject.createArtifacts(artifactFactory,
                    Artifact.SCOPE_RUNTIME, new TypeArtifactFilter("jar"));
        } catch (InvalidDependencyVersionException e) {
            throw new MojoExecutionException("Unable to get project dependencies for "
                    + synapseCore, e);
        }
        log.debug("Direct runtime dependencies for " + synapseCore + " :");
        logArtifacts(synapseRuntimeDeps);
        
        log.debug("Resolving transitive dependencies for " + synapseCore + " ...");
        synapseRuntimeDeps = artifactCollector.collect(synapseRuntimeDeps,
                synapseCoreProject.getArtifact(), synapseCoreProject.getManagedVersionMap(),
                localRepository, remoteArtifactRepositories, artifactMetadataSource, null,
                Collections.<ResolutionListener>singletonList(new DebugResolutionListener(logger))).getArtifacts();
        log.debug("All runtime dependencies for " + synapseCore + " :");
        logArtifacts(synapseRuntimeDeps);
        
        return synapseRuntimeDeps;
    }
    
    private void logArtifacts(Collection<Artifact> collection) {
        List<Artifact> artifacts = new ArrayList<Artifact>(collection);
        Collections.sort(artifacts, new Comparator<Artifact>() {
            @Override
            public int compare(Artifact o1, Artifact o2) {
                return o1.getArtifactId().compareTo(o2.getArtifactId());
            }
        });
        for (Artifact artifact : artifacts) {
            getLog().debug("  " + artifact.getArtifactId() + "-" + artifact.getVersion()
                    + "." + artifact.getArtifactHandler().getExtension());
        }
    }
    
    private static Set<Artifact> filterArtifacts(Set<Artifact> artifacts, ArtifactFilter filter) {
        Set<Artifact> result = new HashSet<Artifact>();
        for (Artifact artifact : artifacts) {
            if (filter.include(artifact)) {
                result.add(artifact);
            }
        }
        return result;
    }
}
