package org.codehaus.mojo.versions;

/*
 * 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.
 */

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.stream.XMLStreamException;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.codehaus.mojo.versions.api.ArtifactAssociation;
import org.codehaus.mojo.versions.api.PomHelper;
import org.codehaus.mojo.versions.api.PropertyVersions;
import org.codehaus.mojo.versions.rewriting.ModifiedPomXMLEventReader;

/**
 * Compare dependency versions of the current project to dependencies or dependency management of a remote repository
 * project. Can optionally update locally the project instead of reporting the comparison
 *
 * @author Paul Gier
 * @since 1.3
 */
@Mojo( name = "compare-dependencies", requiresProject = true, requiresDirectInvocation = true, threadSafe = true )
public class CompareDependenciesMojo
    extends AbstractVersionsDependencyUpdaterMojo
{

    /**
     * The width to pad info messages.
     *
     * @since 1.0-alpha-1
     */
    private static final int INFO_PAD_SIZE = 68;

    /**
     * The groupId, artifactId, and version of the remote project (POM) to which we are comparing. This should be in the
     * form "groupId:artifactId:version"
     */
    @Parameter( property = "remotePom", required = true )
    protected String remotePom;

    /**
     * Ignore the list of remote dependencies and only compare the remote dependencyManagement
     */
    @Parameter( property = "ignoreRemoteDependencies", defaultValue = "false" )
    protected boolean ignoreRemoteDependencies;

    /**
     * Ignore the remote dependency management and only check against the actual dependencies of the remote project
     */
    @Parameter( property = "ignoreRemoteDependencyManagement", defaultValue = "false" )
    protected boolean ignoreRemoteDependencyManagement;

    /**
     * Update dependency versions in the current POM.
     */
    @Parameter( property = "updateDependencies", defaultValue = "false" )
    protected boolean updateDependencies;

    /**
     * Update dependency versions stored in properties
     */
    @Parameter( property = "updatePropertyVersions", defaultValue = "false" )
    protected boolean updatePropertyVersions;

    /**
     * Display the dependency version differences on the command line, but do not update the versions in the current
     * pom. If updateDependencies is set to "true" this will automatically be set to false.
     */
    @Parameter( property = "reportMode", defaultValue = "true" )
    protected boolean reportMode;

    /**
     * If the output file is set, the diff report will be written to this file.
     */
    @Parameter( property = "reportOutputFile" )
    protected File reportOutputFile;

    /**
     * The project builder used to initialize the remote project.
     */
    @Component
    protected MavenProjectBuilder mavenProjectBuilder;

    // ------------------------------ METHODS --------------------------

    /**
     * @param pom the pom to update.
     * @throws org.apache.maven.plugin.MojoExecutionException Something wrong with the plugin itself
     * @throws org.apache.maven.plugin.MojoFailureException The plugin detected an error in the build
     * @throws javax.xml.stream.XMLStreamException when things go wrong with XML streaming
     * @see AbstractVersionsUpdaterMojo#update(org.codehaus.mojo.versions.rewriting.ModifiedPomXMLEventReader)
     */
    protected void update( ModifiedPomXMLEventReader pom )
        throws MojoExecutionException, MojoFailureException, XMLStreamException
    {
        if ( this.ignoreRemoteDependencies && this.ignoreRemoteDependencyManagement )
        {
            throw new MojoFailureException( " ignoreRemoteDependencies and ignoreRemoteDependencyManagement"
                + "are both set to true.  At least one of these needs to be false " );
        }

        if ( updateDependencies )
        {
            reportMode = false;
        }

        String[] remotePomParts = this.remotePom.split( ":" );
        if ( remotePomParts.length != 3 )
        {
            throw new MojoFailureException( " Invalid format for remotePom: " + remotePom );
        }
        String rGroupId = remotePomParts[0];
        String rArtifactId = remotePomParts[1];
        String rVersion = remotePomParts[2];

        Dependency remoteDependency = new Dependency();
        remoteDependency.setGroupId( rGroupId );
        remoteDependency.setArtifactId( rArtifactId );
        remoteDependency.setVersion( rVersion );

        Artifact remoteArtifact = this.toArtifact( remoteDependency );
        MavenProject remoteMavenProject = null;
        try
        {
            remoteMavenProject =
                mavenProjectBuilder.buildFromRepository( remoteArtifact, remoteArtifactRepositories, localRepository );
        }
        catch ( ProjectBuildingException e )
        {
            throw new MojoExecutionException( "Unable to build remote project " + remoteArtifact, e );
        }

        Map<String, Dependency> remoteDepsMap = new HashMap<String, Dependency>();
        if ( !ignoreRemoteDependencyManagement )
        {
            List<Dependency> remoteProjectDepMgmtDeps = ( remoteMavenProject.getDependencyManagement() == null ) ? null
                            : remoteMavenProject.getDependencyManagement().getDependencies();
            mapDependencies( remoteDepsMap, remoteProjectDepMgmtDeps );
        }
        if ( !ignoreRemoteDependencies )
        {
            List<Dependency> remoteProjectDeps = remoteMavenProject.getDependencies();
            mapDependencies( remoteDepsMap, remoteProjectDeps );
        }

        List<String> totalDiffs = new ArrayList<String>();
        List<String> propertyDiffs = new ArrayList<String>();
        if ( getProject().getDependencyManagement() != null && isProcessingDependencyManagement() )
        {
            List<String> depManDiffs =
                compareVersions( pom, getProject().getDependencyManagement().getDependencies(), remoteDepsMap );
            totalDiffs.addAll( depManDiffs );
        }
        if ( getProject().getDependencies() != null && isProcessingDependencies() )
        {
            List<String> depDiffs = compareVersions( pom, getProject().getDependencies(), remoteDepsMap );
            totalDiffs.addAll( depDiffs );
        }
        if ( updatePropertyVersions )
        {
            Map<Property, PropertyVersions> versionProperties =
                this.getHelper().getVersionPropertiesMap( getProject(), null, null, null, true );
            List<String> diff = updatePropertyVersions( pom, versionProperties, remoteDepsMap );
            propertyDiffs.addAll( diff );
        }

        if ( reportMode )
        {
            getLog().info( "The following differences were found:" );
            if ( totalDiffs.size() == 0 )
            {
                getLog().info( "  none" );
            }
            else
            {
                for ( String totalDiff : totalDiffs )
                {
                    getLog().info( "  " + totalDiff );
                }
            }
            getLog().info( "The following property differences were found:" );
            if ( propertyDiffs.size() == 0 )
            {
                getLog().info( "  none" );
            }
            else
            {
                for ( String propertyDiff : propertyDiffs )
                {
                    getLog().info( "  " + propertyDiff );
                }
            }
        }

        if ( reportOutputFile != null )
        {
            writeReportFile( totalDiffs, propertyDiffs );
        }

    }

    /**
     * Compare the dependency versions of the current project with the dependency versions of a remote project
     *
     * @throws XMLStreamException
     */
    private List<String> compareVersions( ModifiedPomXMLEventReader pom, List<Dependency> dependencies,
                                          Map<String, Dependency> remoteDependencies )
        throws MojoExecutionException, XMLStreamException
    {
        List<String> updates = new ArrayList<>();
        for ( Dependency dep : dependencies )
        {
            Artifact artifact = this.toArtifact( dep );
            if ( !isIncluded( artifact ) )
            {
                continue;
            }

            Dependency remoteDep = remoteDependencies.get( dep.getManagementKey() );
            if ( remoteDep != null )
            {
                String remoteVersion = remoteDep.getVersion();

                if ( !dep.getVersion().equals( remoteVersion ) )
                {
                    StringBuilder buf = writeDependencyDiffMessage( dep, remoteVersion );
                    updates.add( buf.toString() );
                    if ( !reportMode )
                    {
                        if ( PomHelper.setDependencyVersion( pom, dep.getGroupId(), dep.getArtifactId(),
                                                             dep.getVersion(), remoteVersion,
                                                             getProject().getModel() ) )
                        {
                            getLog().info( "Updated " + toString( dep ) + " to version " + remoteVersion );
                        }
                    }

                }

            }
        }

        return updates;

    }

    /**
     * Updates the properties holding a version if necessary.
     */
    private List<String> updatePropertyVersions( ModifiedPomXMLEventReader pom,
                                                 Map<Property, PropertyVersions> versionProperties,
                                                 Map<String, Dependency> remoteDependencies )
        throws XMLStreamException
    {
        List<String> result = new ArrayList<>();
        for ( Map.Entry<Property, PropertyVersions> entry : versionProperties.entrySet() )
        {
            Property property = entry.getKey();
            PropertyVersions version = entry.getValue();

            String candidateVersion = computeCandidateVersion( remoteDependencies, property, version );
            if ( candidateVersion != null )
            {
                String originalVersion = version.getAssociations()[0].getArtifact().getVersion(); // Yekes
                if ( !candidateVersion.equals( originalVersion ) ) // Update needed
                {
                    result.add( writeDiffMessage( property.getName(), originalVersion, candidateVersion ).toString() );
                    if ( !reportMode
                        && PomHelper.setPropertyVersion( pom, null, property.getName(), candidateVersion ) )
                    {
                        getLog().info( "Updated ${" + property.getName() + "} from " + originalVersion + " to "
                            + candidateVersion );
                    }
                }
            }
        }
        return result;
    }

    /**
     * Returns the candidate version to use for the specified property.
     * <p/>
     * The dependencies currently linked to the property must all be defined by the remote POM and they should refer to
     * the same version. If that's the case, that same version is returned. Otherwise, <tt>null</tt> is returned
     * indicating that there is no candidate.
     *
     * @param remoteDependencies the remote dependencies
     * @param property the property to update
     * @param propertyVersions the association
     * @return the candidate version or <tt>null</tt> if there isn't any
     */
    private String computeCandidateVersion( Map<String, Dependency> remoteDependencies, Property property,
                                            PropertyVersions propertyVersions )
    {
        String candidateVersion = null;
        for ( ArtifactAssociation artifactAssociation : propertyVersions.getAssociations() )
        {
            String id = generateId( artifactAssociation.getArtifact() );
            Dependency dependency = remoteDependencies.get( id );
            if ( dependency == null )
            {
                getLog().info( "Not updating ${" + property.getName() + "}: no info for " + id );
                return null;
            }
            else
            {
                if ( candidateVersion == null )
                {
                    candidateVersion = dependency.getVersion();
                }
                else if ( !candidateVersion.equals( dependency.getVersion() ) )
                {
                    getLog().warn( "Could not update ${" + property.getName() + "}: version mismatch" );
                    return null;
                }
            }
        }
        return candidateVersion;
    }

    private void writeReportFile( List<String> dependenciesUpdate, List<String> propertiesUpdate )
        throws MojoExecutionException
    {
        if ( !reportOutputFile.getParentFile().exists() )
        {
            reportOutputFile.getParentFile().mkdirs();
        }

        try( FileWriter fw = new FileWriter( reportOutputFile ); //
            PrintWriter pw = new PrintWriter( fw ) )
        {
            pw.println( "The following differences were found:" );
            pw.println();
            if ( dependenciesUpdate.size() == 0 )
            {
                pw.println( "  none" );
            }
            else
            {
                for ( String dependencyUpdate : dependenciesUpdate )
                {
                    pw.println( "  " + dependencyUpdate );
                }
            }
            pw.println();
            pw.println( "The following property differences were found:" );
            pw.println();
            if ( propertiesUpdate.size() == 0 )
            {
                pw.println( "  none" );
            }
            else
            {
                for ( String propertyUpdate : propertiesUpdate )
                {
                    pw.println( "  " + propertyUpdate );
                }
            }
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "Unable to write report file. ", e );
        }
    }

    /**
     * Create a simple message describing the version diff
     *
     * @param dep
     * @param remoteVersion
     * @return The message
     */
    private StringBuilder writeDependencyDiffMessage( Dependency dep, String remoteVersion )
    {
        String id = dep.getGroupId() + ":" + dep.getArtifactId();
        return writeDiffMessage( id, dep.getVersion(), remoteVersion );
    }

    private StringBuilder writeDiffMessage( String id, String originalVersion, String targetVersion )
    {
        StringBuilder buf = new StringBuilder();
        buf.append( id );
        buf.append( ' ' );
        int padding = INFO_PAD_SIZE - originalVersion.length() - targetVersion.length() - 4;
        while ( buf.length() < padding )
        {
            buf.append( '.' );
        }
        buf.append( ' ' );
        buf.append( originalVersion );
        buf.append( " -> " );
        buf.append( targetVersion );
        return buf;
    }

    /**
     * Add a list of dependencies to a Map for easy access
     *
     * @param map
     * @param deps
     */
    private void mapDependencies( Map<String, Dependency> map, List<Dependency> deps )
    {
        if ( deps != null )
        {
            for ( Dependency nextDep : deps )
            {
                map.put( nextDep.getManagementKey(), nextDep );
            }
        }
    }

    /**
     * Creates a key that is similar to what {@link Dependency#getManagementKey()} generates for a dependency.
     */
    private static String generateId( Artifact artifact )
    {
        StringBuilder sb = new StringBuilder();
        sb.append( artifact.getGroupId() ).append( ":" ).append( artifact.getArtifactId() ).append( ":" ).append( artifact.getType() );
        if ( artifact.getClassifier() != null )
        {
            sb.append( ":" ).append( artifact.getClassifier() );
        }
        return sb.toString();
    }

}