package org.apache.maven.shared.artifact.filter;

/*
 * 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.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.codehaus.plexus.logging.Logger;

/**
 * TODO: include in maven-artifact in future
 * 
 * @author <a href="mailto:brett@apache.org">Brett Porter</a>
 * @see StrictPatternIncludesArtifactFilter
 */
public class PatternIncludesArtifactFilter
    implements ArtifactFilter, StatisticsReportingArtifactFilter
{
    private final List<String> positivePatterns;

    private final List<String> negativePatterns;

    private final boolean actTransitively;

    private final Set<String> patternsTriggered = new HashSet<>();

    private final List<String> filteredArtifactIds = new ArrayList<>();

    /**
     * @param patterns The pattern to be used.
     */
    public PatternIncludesArtifactFilter( final Collection<String> patterns )
    {
        this( patterns, false );
    }

    /**
     * @param patterns The pattern to be used.
     * @param actTransitively transitive yes/no.
     */
    public PatternIncludesArtifactFilter( final Collection<String> patterns, final boolean actTransitively )
    {
        this.actTransitively = actTransitively;
        final List<String> pos = new ArrayList<>();
        final List<String> neg = new ArrayList<>();
        if ( patterns != null && !patterns.isEmpty() )
        {
            for ( String pattern : patterns )
            {
                if ( pattern.startsWith( "!" ) )
                {
                    neg.add( pattern.substring( 1 ) );
                }
                else
                {
                    pos.add( pattern );
                }
            }
        }

        positivePatterns = pos;
        negativePatterns = neg;
    }

    /** {@inheritDoc} */
    public boolean include( final Artifact artifact )
    {
        final boolean shouldInclude = patternMatches( artifact );

        if ( !shouldInclude )
        {
            addFilteredArtifactId( artifact.getId() );
        }

        return shouldInclude;
    }

    /**
     * @param artifact to check for.
     * @return true if the match is true false otherwise.
     */
    protected boolean patternMatches( final Artifact artifact )
    {
        return positiveMatch( artifact ) == Boolean.TRUE || negativeMatch( artifact ) == Boolean.FALSE;
    }

    /**
     * @param artifactId add artifact to the filtered artifacts list.
     */
    protected void addFilteredArtifactId( final String artifactId )
    {
        filteredArtifactIds.add( artifactId );
    }

    private Boolean negativeMatch( final Artifact artifact )
    {
        if ( negativePatterns == null || negativePatterns.isEmpty() )
        {
            return null;
        }
        else
        {
            return match( artifact, negativePatterns );
        }
    }

    /**
     * @param artifact check for positive match.
     * @return true/false.
     */
    protected Boolean positiveMatch( final Artifact artifact )
    {
        if ( positivePatterns == null || positivePatterns.isEmpty() )
        {
            return null;
        }
        else
        {
            return match( artifact, positivePatterns );
        }
    }

    private boolean match( final Artifact artifact, final List<String> patterns )
    {
        final String shortId = ArtifactUtils.versionlessKey( artifact );
        final String id = artifact.getDependencyConflictId();
        final String wholeId = artifact.getId();

        if ( matchAgainst( wholeId, patterns, false ) )
        {
            return true;
        }

        if ( matchAgainst( id, patterns, false ) )
        {
            return true;
        }

        if ( matchAgainst( shortId, patterns, false ) )
        {
            return true;
        }

        if ( actTransitively )
        {
            final List<String> depTrail = artifact.getDependencyTrail();

            if ( depTrail != null && depTrail.size() > 1 )
            {
                for ( String trailItem : depTrail )
                {
                    if ( matchAgainst( trailItem, patterns, true ) )
                    {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    private boolean matchAgainst( final String value, final List<String> patterns, final boolean regionMatch )
    {
        final String[] tokens = value.split( ":" );
        for ( String pattern : patterns )
        {
            String[] patternTokens = pattern.split( ":" );
            
            if ( patternTokens.length == 5 && tokens.length < 5 )
            {
                // 4th element is the classifier
                if ( !"*".equals( patternTokens[3] ) )
                {
                    // classifier required, cannot be a match
                    return false;
                }
                patternTokens = new String[] { patternTokens[0], patternTokens[1], patternTokens[2], patternTokens[4] };
            }

            // fail immediately if pattern tokens outnumber tokens to match
            boolean matched = patternTokens.length <= tokens.length;

            for ( int i = 0; matched && i < patternTokens.length; i++ )
            {
                matched = matches( tokens[i], patternTokens[i] );
            }

            // case of starting '*' like '*:jar:*'
            // This really only matches from the end instead.....
            if ( !matched && patternTokens.length < tokens.length && isFirstPatternWildcard( patternTokens ) )
            {
                matched = true;
                int tokenOffset = tokens.length - patternTokens.length;
                for ( int i = 0; matched && i < patternTokens.length; i++ )
                {
                    matched = matches( tokens[i + tokenOffset], patternTokens[i] );
                }
            }

            if ( matched )
            {
                patternsTriggered.add( pattern );
                return true;
            }

            if ( regionMatch && value.contains( pattern ) )
            {
                patternsTriggered.add( pattern );
                return true;
            }

        }
        return false;

    }

    private boolean isFirstPatternWildcard( String[] patternTokens )
    {
        return patternTokens.length > 0 && "*".equals( patternTokens[0] );
    }

    /**
     * Gets whether the specified token matches the specified pattern segment.
     * 
     * @param token the token to check
     * @param pattern the pattern segment to match, as defined above
     * @return <code>true</code> if the specified token is matched by the specified pattern segment
     */
    private boolean matches( final String token, final String pattern )
    {
        boolean matches;

        // support full wildcard and implied wildcard
        if ( "*".equals( pattern ) || pattern.length() == 0 )
        {
            matches = true;
        }
        // support contains wildcard
        else if ( pattern.startsWith( "*" ) && pattern.endsWith( "*" ) )
        {
            final String contains = pattern.substring( 1, pattern.length() - 1 );

            matches = token.contains( contains );
        }
        // support leading wildcard
        else if ( pattern.startsWith( "*" ) )
        {
            final String suffix = pattern.substring( 1 );

            matches = token.endsWith( suffix );
        }
        // support trailing wildcard
        else if ( pattern.endsWith( "*" ) )
        {
            final String prefix = pattern.substring( 0, pattern.length() - 1 );

            matches = token.startsWith( prefix );
        }
        // support wildcards in the middle of a pattern segment
        else if ( pattern.indexOf( '*' ) > -1 )
        {
            String[] parts = pattern.split( "\\*" );
            int lastPartEnd = -1;
            boolean match = true;

            for ( String part : parts )
            {
                int idx = token.indexOf( part );
                if ( idx <= lastPartEnd )
                {
                    match = false;
                    break;
                }

                lastPartEnd = idx + part.length();
            }

            matches = match;
        }
        // support versions range
        else if ( pattern.startsWith( "[" ) || pattern.startsWith( "(" ) )
        {
            matches = isVersionIncludedInRange( token, pattern );
        }
        // support exact match
        else
        {
            matches = token.equals( pattern );
        }

        return matches;
    }

    private boolean isVersionIncludedInRange( final String version, final String range )
    {
        try
        {
            return VersionRange.createFromVersionSpec( range ).containsVersion( new DefaultArtifactVersion( version ) );
        }
        catch ( final InvalidVersionSpecificationException e )
        {
            return false;
        }
    }

    /** {@inheritDoc} */
    public void reportMissedCriteria( final Logger logger )
    {
        // if there are no patterns, there is nothing to report.
        if ( !positivePatterns.isEmpty() || !negativePatterns.isEmpty() )
        {
            final List<String> missed = new ArrayList<>();
            missed.addAll( positivePatterns );
            missed.addAll( negativePatterns );

            missed.removeAll( patternsTriggered );

            if ( !missed.isEmpty() && logger.isWarnEnabled() )
            {
                final StringBuilder buffer = new StringBuilder();

                buffer.append( "The following patterns were never triggered in this " );
                buffer.append( getFilterDescription() );
                buffer.append( ':' );

                for ( String pattern : missed )
                {
                    buffer.append( "\no  '" ).append( pattern ).append( "'" );
                }

                buffer.append( "\n" );

                logger.warn( buffer.toString() );
            }
        }
    }

    @Override
    public String toString()
    {
        return "Includes filter:" + getPatternsAsString();
    }

    /**
     * @return pattern as a string.
     */
    protected String getPatternsAsString()
    {
        final StringBuilder buffer = new StringBuilder();
        for ( String pattern : positivePatterns )
        {
            buffer.append( "\no '" ).append( pattern ).append( "'" );
        }

        return buffer.toString();
    }

    /**
     * @return description.
     */
    protected String getFilterDescription()
    {
        return "artifact inclusion filter";
    }

    /** {@inheritDoc} */
    public void reportFilteredArtifacts( final Logger logger )
    {
        if ( !filteredArtifactIds.isEmpty() && logger.isDebugEnabled() )
        {
            final StringBuilder buffer =
                new StringBuilder( "The following artifacts were removed by this " + getFilterDescription() + ": " );

            for ( String artifactId : filteredArtifactIds )
            {
                buffer.append( '\n' ).append( artifactId );
            }

            logger.debug( buffer.toString() );
        }
    }

    /** {@inheritDoc} */
    public boolean hasMissedCriteria()
    {
        // if there are no patterns, there is nothing to report.
        if ( !positivePatterns.isEmpty() || !negativePatterns.isEmpty() )
        {
            final List<String> missed = new ArrayList<>();
            missed.addAll( positivePatterns );
            missed.addAll( negativePatterns );

            missed.removeAll( patternsTriggered );

            return !missed.isEmpty();
        }

        return false;
    }

}
