package org.codehaus.mojo.versions.api;

/*
 * 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 org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.plugin.MojoExecutionException;
import org.codehaus.mojo.versions.Property;
import org.codehaus.mojo.versions.ordering.InvalidSegmentException;
import org.codehaus.mojo.versions.ordering.VersionComparator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * Manages a property that is associated with one or more artifacts.
 *
 * @author Stephen Connolly
 * @since 1.0-alpha-3
 */
public class PropertyVersions
    extends AbstractVersionDetails
{
    private final String name;

    private final String profileId;

    private final Set<ArtifactAssociation> associations;

    private final VersionsHelper helper;

    /**
     * The available versions.
     *
     * @since 1.0-beta-1
     */
    private final SortedSet<ArtifactVersion> versions;

    private final PropertyVersions.PropertyVersionComparator comparator;

    PropertyVersions( String profileId, String name, VersionsHelper helper, Set<ArtifactAssociation> associations )
        throws ArtifactMetadataRetrievalException
    {
        this.profileId = profileId;
        this.name = name;
        this.helper = helper;
        this.associations = new TreeSet<>( associations );
        this.comparator = new PropertyVersionComparator();
        this.versions = resolveAssociatedVersions( helper, associations, comparator );

    }

    private static SortedSet<ArtifactVersion> resolveAssociatedVersions( VersionsHelper helper,
                                                                         Set<ArtifactAssociation> associations,
                                                                         VersionComparator versionComparator )
        throws ArtifactMetadataRetrievalException
    {
        SortedSet<ArtifactVersion> versions = null;
        for ( ArtifactAssociation association : associations )
        {
            final ArtifactVersions associatedVersions =
                helper.lookupArtifactVersions( association.getArtifact(), association.isUsePluginRepositories() );
            if ( versions != null )
            {
                final ArtifactVersion[] artifactVersions = associatedVersions.getVersions( true );
                // since ArtifactVersion does not override equals, we have to do this the hard way
                // result.retainAll( Arrays.asList( artifactVersions ) );
                Iterator j = versions.iterator();
                while ( j.hasNext() )
                {
                    boolean contains = false;
                    ArtifactVersion version = (ArtifactVersion) j.next();
                    for ( ArtifactVersion artifactVersion : artifactVersions )
                    {
                        if ( version.compareTo( artifactVersion ) == 0 )
                        {
                            contains = true;
                            break;
                        }
                    }
                    if ( !contains )
                    {
                        j.remove();
                    }
                }
            }
            else
            {
                versions = new TreeSet<>( versionComparator );
                versions.addAll( Arrays.asList( associatedVersions.getVersions( true ) ) );
            }
        }
        if ( versions == null )
        {
            versions = new TreeSet<>( versionComparator );
        }
        return Collections.unmodifiableSortedSet( versions );
    }

    /**
     * Gets the rule for version comparison of this artifact.
     *
     * @return the rule for version comparison of this artifact.
     * @since 1.0-beta-1
     */
    public VersionComparator getVersionComparator()
    {
        return comparator;
    }

    public ArtifactAssociation[] getAssociations()
    {
        return associations.toArray( new ArtifactAssociation[associations.size()] );
    }

    private VersionComparator[] lookupComparators()
    {
        Set<VersionComparator> result = new HashSet();
        Iterator<ArtifactAssociation> i = associations.iterator();
        while ( i.hasNext() )
        {
            result.add( helper.getVersionComparator( i.next().getArtifact() ) );
        }
        return result.toArray( new VersionComparator[result.size()] );
    }

    /**
     * Uses the supplied {@link Collection} of {@link Artifact} instances to see if an ArtifactVersion can be provided.
     *
     * @param artifacts The {@link Collection} of {@link Artifact} instances .
     * @return The versions that can be resolved from the supplied Artifact instances or an empty array if no version
     *         can be resolved (i.e. the property is not associated with any of the supplied artifacts or the property
     *         is also associated to an artifact that has not been provided).
     * @throws org.apache.maven.plugin.MojoExecutionException When things go wrong.
     * @since 1.0-alpha-3
     */
    public ArtifactVersion[] getVersions( Collection<Artifact> artifacts )
        throws MojoExecutionException
    {
        List<ArtifactVersion> result = new ArrayList<>();
        // go through all the associations
        // see if they are met from the collection
        // add the version if they are
        // go through all the versions
        // see if the version is available for all associations
        for ( ArtifactAssociation association : associations )
        {
            for ( Artifact artifact : artifacts )
            {
                if ( association.getGroupId().equals( artifact.getGroupId() )
                    && association.getArtifactId().equals( artifact.getArtifactId() ) )
                {
                    try
                    {
                        result.add( artifact.getSelectedVersion() );
                    }
                    catch ( OverConstrainedVersionException e )
                    {
                        // ignore this one as we cannot resolve a valid version
                    }
                }
            }
        }
        // we now have a list of all the versions that partially satisfy the association requirements
        Iterator<ArtifactVersion> k = result.iterator();
        versions: while ( k.hasNext() )
        {
            ArtifactVersion candidate = k.next();
            associations: for ( ArtifactAssociation association : associations )
            {
                for ( Artifact artifact : artifacts )
                {
                    if ( association.getGroupId().equals( artifact.getGroupId() )
                        && association.getArtifactId().equals( artifact.getArtifactId() ) )
                    {
                        try
                        {
                            if ( candidate.toString().equals( artifact.getSelectedVersion().toString() ) )
                            {
                                // this association can be met, try the next
                                continue associations;
                            }
                        }
                        catch ( OverConstrainedVersionException e )
                        {
                            // ignore this one again
                        }
                    }
                }
                // candidate is not valid as at least one association cannot be met
                k.remove();
                continue versions;
            }
        }
        return asArtifactVersionArray( result );
    }

    /**
     * Uses the {@link DefaultVersionsHelper} to find all available versions that match all the associations with this
     * property.
     *
     * @param includeSnapshots Whether to include snapshot versions in our search.
     * @return The (possibly empty) array of versions.
     */
    public synchronized ArtifactVersion[] getVersions( boolean includeSnapshots )
    {
        Set<ArtifactVersion> result;
        if ( includeSnapshots )
        {
            result = versions;
        }
        else
        {
            result = new TreeSet<>( getVersionComparator() );
            for ( ArtifactVersion candidate : versions )
            {
                if ( ArtifactUtils.isSnapshot( candidate.toString() ) )
                {
                    continue;
                }
                result.add( candidate );
            }
        }
        return asArtifactVersionArray( result );
    }

    private ArtifactVersion[] asArtifactVersionArray( Collection<ArtifactVersion> result )
    {
        if ( result == null || result.isEmpty() )
        {
            return new ArtifactVersion[0];
        }
        else
        {
            final ArtifactVersion[] answer = result.toArray( new ArtifactVersion[result.size()] );
            VersionComparator[] rules = lookupComparators();
            assert rules.length > 0;
            Arrays.sort( answer, rules[0] );
            if ( rules.length == 1 || answer.length == 1 )
            {
                // only one rule...
                return answer;
            }
            ArtifactVersion[] alt = answer.clone();
            for ( int j = 1; j < rules.length; j++ )
            {
                Arrays.sort( alt, rules[j] );
                if ( !Arrays.equals( alt, answer ) )
                {
                    throw new IllegalStateException( "Property " + name + " is associated with multiple artifacts"
                        + " and these artifacts use different version sorting rules and these rules are effectively"
                        + " incompatible for the set of versions available to this property.\nFirst rule says: "
                        + Arrays.asList( answer ) + "\nSecond rule says: " + Arrays.asList( alt ) );
                }
            }
            return answer;
        }
    }

    public String getName()
    {
        return name;
    }

    public String getProfileId()
    {
        return profileId;
    }

    public boolean isAssociated()
    {
        return !associations.isEmpty();
    }

    public String toString()
    {
        return "PropertyVersions{" + ( profileId == null ? "" : "profileId='" + profileId + "', " ) + "name='" + name
            + '\'' + ", associations=" + associations + '}';
    }

    public ArtifactVersion getNewestVersion( String currentVersion, Property property, boolean allowSnapshots,
                                             List reactorProjects, VersionsHelper helper )
        throws MojoExecutionException
    {
        return getNewestVersion( currentVersion, property, allowSnapshots, reactorProjects, helper, false, -1 );
    }

    public ArtifactVersion getNewestVersion( String currentVersion, Property property, boolean allowSnapshots,
                                             List reactorProjects, VersionsHelper helper, boolean allowDowngrade,
                                             int segment )
        throws MojoExecutionException
    {
        final boolean includeSnapshots = !property.isBanSnapshots() && allowSnapshots;
        helper.getLog().debug( "getNewestVersion(): includeSnapshots='" + includeSnapshots + "'" );
        helper.getLog().debug( "Property ${" + property.getName() + "}: Set of valid available versions is "
            + Arrays.asList( getVersions( includeSnapshots ) ) );
        VersionRange range;
        try
        {
            if ( property.getVersion() != null )
            {
                range = VersionRange.createFromVersionSpec( property.getVersion() );
                helper.getLog().debug( "Property ${" + property.getName() + "}: Restricting results to " + range );
            }
            else
            {
                range = null;
                helper.getLog().debug( "Property ${" + property.getName() + "}: Restricting results to " + range );
            }
        }
        catch ( InvalidVersionSpecificationException e )
        {
            throw new MojoExecutionException( e.getMessage(), e );
        }

        ArtifactVersion lowerBoundArtifactVersion = null;
        if ( allowDowngrade )
        {
            if ( segment != -1 )
            {
                lowerBoundArtifactVersion = getLowerBound(helper, currentVersion, segment);
            }
            helper.getLog().debug( "lowerBoundArtifactVersion is null based on allowDowngrade:" + allowDowngrade );
        }
        else
        {
            lowerBoundArtifactVersion = helper.createArtifactVersion( currentVersion );
            helper.getLog().debug( "lowerBoundArtifactVersion: " + lowerBoundArtifactVersion.toString() );
        }

        ArtifactVersion upperBound = null;
        if ( segment != -1 )
        {
            upperBound = getVersionComparator().incrementSegment( lowerBoundArtifactVersion, segment );
            helper.getLog().debug( "Property ${" + property.getName() + "}: upperBound is: " + upperBound );
        }
        ArtifactVersion result =
            getNewestVersion( range, lowerBoundArtifactVersion, upperBound, includeSnapshots, false, false );

        helper.getLog().debug( "Property ${" + property.getName() + "}: Current winner is: " + result );

        if ( property.isSearchReactor() )
        {
            helper.getLog().debug( "Property ${" + property.getName() + "}: Searching reactor for a valid version..." );
            Collection reactorArtifacts = helper.extractArtifacts( reactorProjects );
            ArtifactVersion[] reactorVersions = getVersions( reactorArtifacts );
            helper.getLog().debug( "Property ${" + property.getName()
                + "}: Set of valid available versions from the reactor is " + Arrays.asList( reactorVersions ) );
            ArtifactVersion fromReactor = null;
            if ( reactorVersions.length > 0 )
            {
                for ( int j = reactorVersions.length - 1; j >= 0; j-- )
                {
                    if ( range == null || ArtifactVersions.isVersionInRange( reactorVersions[j], range ) )
                    {
                        fromReactor = reactorVersions[j];
                        helper.getLog().debug( "Property ${" + property.getName() + "}: Reactor has version "
                            + fromReactor );
                        break;
                    }
                }
            }
            if ( fromReactor != null && ( result != null || !currentVersion.equals( fromReactor.toString() ) ) )
            {
                if ( property.isPreferReactor() )
                {
                    helper.getLog().debug( "Property ${" + property.getName()
                        + "}: Reactor has a version and we prefer the reactor" );
                    result = fromReactor;
                }
                else
                {
                    if ( result == null )
                    {
                        helper.getLog().debug( "Property ${" + property.getName() + "}: Reactor has the only version" );
                        result = fromReactor;
                    }
                    else if ( getVersionComparator().compare( result, fromReactor ) < 0 )
                    {
                        helper.getLog().debug( "Property ${" + property.getName() + "}: Reactor has a newer version" );
                        result = fromReactor;
                    }
                    else
                    {
                        helper.getLog().debug( "Property ${" + property.getName()
                            + "}: Reactor has the same or older version" );
                    }
                }
            }
        }
        return result;
    }

    private ArtifactVersion getNewestVersion( String currentVersion, VersionsHelper helper, int segment,
                                              boolean includeSnapshots, VersionRange range )
    {
        ArtifactVersion lowerBound = helper.createArtifactVersion( currentVersion );
        ArtifactVersion upperBound = null;
        if ( segment != -1 )
        {
            upperBound = getVersionComparator().incrementSegment( lowerBound, segment );
        }
        return getNewestVersion( range, lowerBound, upperBound, includeSnapshots, false, false );
    }

    private final class PropertyVersionComparator
        implements VersionComparator
    {
        public int compare( ArtifactVersion v1, ArtifactVersion v2 )
        {
            return innerCompare( v1, v2 );
        }

        private int innerCompare( ArtifactVersion v1, ArtifactVersion v2 )
        {
            if ( !isAssociated() )
            {
                throw new IllegalStateException( "Cannot compare versions for a property with no associations" );
            }
            VersionComparator[] comparators = lookupComparators();
            assert comparators.length >= 1 : "we have at least one association => at least one comparator";
            int result = comparators[0].compare( v1, v2 );
            for ( int i = 1; i < comparators.length; i++ )
            {
                int alt = comparators[i].compare( v1, v2 );
                if ( result != alt && ( result >= 0 && alt < 0 ) || ( result <= 0 && alt > 0 ) )
                {
                    throw new IllegalStateException( "Property " + name + " is associated with multiple artifacts"
                        + " and these artifacts use different version sorting rules and these rules are effectively"
                        + " incompatible for the two of versions being compared.\nFirst rule says compare(\"" + v1
                        + "\", \"" + v2 + "\") = " + result + "\nSecond rule says compare(\"" + v1 + "\", \"" + v2
                        + "\") = " + alt );
                }
            }
            return result;
        }

        public int getSegmentCount( ArtifactVersion v )
        {
            if ( !isAssociated() )
            {
                throw new IllegalStateException( "Cannot compare versions for a property with no associations" );
            }
            VersionComparator[] comparators = lookupComparators();
            assert comparators.length >= 1 : "we have at least one association => at least one comparator";
            int result = comparators[0].getSegmentCount( v );
            for ( int i = 1; i < comparators.length; i++ )
            {
                int alt = comparators[i].getSegmentCount( v );
                if ( result != alt )
                {
                    throw new IllegalStateException( "Property " + name + " is associated with multiple artifacts"
                        + " and these artifacts use different version sorting rules and these rules are effectively"
                        + " incompatible for the two of versions being compared.\nFirst rule says getSegmentCount(\""
                        + v + "\") = " + result + "\nSecond rule says getSegmentCount(\"" + v + "\") = " + alt );
                }
            }
            return result;
        }

        public ArtifactVersion incrementSegment( ArtifactVersion v, int segment )
        {
            if ( !isAssociated() )
            {
                throw new IllegalStateException( "Cannot compare versions for a property with no associations" );
            }
            VersionComparator[] comparators = lookupComparators();
            assert comparators.length >= 1 : "we have at least one association => at least one comparator";
            ArtifactVersion result = comparators[0].incrementSegment( v, segment );
            for ( int i = 1; i < comparators.length; i++ )
            {
                ArtifactVersion alt = comparators[i].incrementSegment( v, segment );
                if ( !result.toString().equals( alt.toString() ) )
                {
                    throw new IllegalStateException( "Property " + name + " is associated with multiple artifacts"
                        + " and these artifacts use different version sorting rules and these rules are effectively"
                        + " incompatible for the two of versions being compared.\nFirst rule says incrementSegment(\""
                        + v + "\", " + segment + ") = " + result + "\nSecond rule says incrementSegment(\"" + v + "\", "
                        + segment + ") = " + alt );
                }
            }
            return result;
        }

    }


    private ArtifactVersion getLowerBound(VersionsHelper helper,
            String currentVersion, int segment)
    {
        ArtifactVersion version = helper.createArtifactVersion(currentVersion);
        int segmentCount = getVersionComparator().getSegmentCount(version);
        if (segment < 0 || segment > segmentCount)
        {
            throw new InvalidSegmentException(segment, segmentCount,
                    currentVersion);
        }

        StringBuilder newVersion = new StringBuilder();
        newVersion.append(segment >= 0 ? version.getMajorVersion() : 0);
        if (segmentCount > 0)
        {
            newVersion.append(".")
                    .append(segment >= 1 ? version.getMinorVersion() : 0);
        }
        if (segmentCount > 1)
        {
            newVersion.append(".")
                    .append(segment >= 2 ? version.getIncrementalVersion() : 0);
        }
        if (segmentCount > 2)
        {
            if (version.getQualifier() != null)
            {
                newVersion.append("-")
                        .append(segment >= 3 ? version.getQualifier() : "0");
            } else
            {
                newVersion.append("-")
                        .append(segment >= 3 ? version.getBuildNumber() : "0");
            }
        }
        return helper.createArtifactVersion(newVersion.toString());
    }

}
