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.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import org.apache.maven.artifact.Artifact;
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.slf4j.Logger;

/**
 * TODO: include in maven-artifact in future
 *
 * @author <a href="mailto:brett@apache.org">Brett Porter</a>
 * @see StrictPatternIncludesArtifactFilter
 */
public class GNPatternIncludesArtifactFilter
    implements ArtifactFilter, StatisticsReportingArtifactFilter
{
    /** Holds the set of compiled patterns */
    private final Set<Pattern> patterns;

    /** Whether the dependency trail should be checked */
    private final boolean actTransitively;

    /** Set of patterns that have been triggered */
    private final Set<Pattern> patternsTriggered = new HashSet<>();

    /** Set of artifacts that have been filtered out */
    private final List<Artifact> filteredArtifact = new ArrayList<>();

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

    /**
     * <p>Constructor for PatternIncludesArtifactFilter.</p>
     *
     * @param patterns The pattern to be used.
     * @param actTransitively transitive yes/no.
     */
    public GNPatternIncludesArtifactFilter( final Collection<String> patterns, final boolean actTransitively )
    {
        this.actTransitively = actTransitively;
        final Set<Pattern> pat = new LinkedHashSet<>();
        if ( patterns != null && !patterns.isEmpty() )
        {
            for ( String pattern : patterns )
            {

                Pattern p = compile( pattern );
                pat.add( p );
            }
        }
        this.patterns = pat;
    }

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

        if ( !shouldInclude )
        {
            addFilteredArtifact( artifact );
        }

        return shouldInclude;
    }

    /**
     * <p>patternMatches.</p>
     *
     * @param artifact to check for.
     * @return true if the match is true false otherwise.
     */
    protected boolean patternMatches( final Artifact artifact )
    {
        // Check if the main artifact matches
        char[][] artifactGatvCharArray = new char[][] {
                            emptyOrChars( artifact.getGroupId() ),
                            emptyOrChars( artifact.getArtifactId() ),
                            emptyOrChars( artifact.getType() ),
                            emptyOrChars( artifact.getClassifier() ),
                            emptyOrChars( artifact.getBaseVersion() )
                    };
        Boolean match = match( artifactGatvCharArray );
        if ( match != null )
        {
            return match;
        }

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

            if ( depTrail != null && depTrail.size() > 1 )
            {
                for ( String trailItem : depTrail )
                {
                    char[][] depGatvCharArray = tokenizeAndSplit( trailItem );
                    match = match( depGatvCharArray );
                    if ( match != null )
                    {
                        return match;
                    }
                }
            }
        }

        return false;
    }

    private Boolean match( char[][] gatvCharArray )
    {
        for ( Pattern pattern : patterns )
        {
            if ( pattern.matches( gatvCharArray ) )
            {
                patternsTriggered.add( pattern );
                return !( pattern instanceof NegativePattern );
            }
        }

        return null;
    }

    /**
     * <p>addFilteredArtifact.</p>
     *
     * @param artifact add artifact to the filtered artifacts list.
     */
    protected void addFilteredArtifact( final Artifact artifact )
    {
        filteredArtifact.add( artifact );
    }

    /** {@inheritDoc} */
    public void reportMissedCriteria( final Logger logger )
    {
        // if there are no patterns, there is nothing to report.
        if ( !patterns.isEmpty() )
        {
            final List<Pattern> missed = new ArrayList<>( patterns );
            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 ( Pattern pattern : missed )
                {
                    buffer.append( "\no  '" ).append( pattern ).append( "'" );
                }

                buffer.append( "\n" );

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

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

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

        return buffer.toString();
    }

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

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

            for ( Artifact artifactId : filteredArtifact )
            {
                buffer.append( '\n' ).append( artifactId.getId() );
            }

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

    /**
     * {@inheritDoc}
     *
     * @return a boolean.
     */
    public boolean hasMissedCriteria()
    {
        // if there are no patterns, there is nothing to report.
        if ( !patterns.isEmpty() )
        {
            final List<Pattern> missed = new ArrayList<>( patterns );
            missed.removeAll( patternsTriggered );

            return !missed.isEmpty();
        }

        return false;
    }

    private static final char[] EMPTY = new char[0];

    private static final char[] ANY = new char[] { '*' };

    static char[] emptyOrChars( String str )
    {
        return str != null && str.length() > 0 ? str.toCharArray() : EMPTY;
    }

    static char[] anyOrChars( char[] str )
    {
        return str.length > 1 || ( str.length == 1 && str[0] != '*' ) ? str : ANY;
    }

    static char[][] tokenizeAndSplit( String pattern )
    {
        String[] stokens = pattern.split( ":" );
        char[][] tokens = new char[ stokens.length ][];
        for ( int i = 0; i < stokens.length; i++ )
        {
            tokens[i] = emptyOrChars( stokens[i] );
        }
        return tokens;
    }

    @SuppressWarnings( "InnerAssignment" )
    static boolean match( char[] patArr, char[] strArr, boolean isVersion )
    {
        int patIdxStart = 0;
        int patIdxEnd = patArr.length - 1;
        int strIdxStart = 0;
        int strIdxEnd = strArr.length - 1;
        char ch;

        boolean containsStar = false;
        for ( char aPatArr : patArr )
        {
            if ( aPatArr == '*' )
            {
                containsStar = true;
                break;
            }
        }

        if ( !containsStar )
        {
            if ( isVersion && ( patArr[0] == '[' || patArr[0] == '(' ) )
            {
                return isVersionIncludedInRange( String.valueOf( strArr ), String.valueOf( patArr ) );
            }
            // No '*'s, so we make a shortcut
            if ( patIdxEnd != strIdxEnd )
            {
                return false; // Pattern and string do not have the same size
            }
            for ( int i = 0; i <= patIdxEnd; i++ )
            {
                ch = patArr[i];
                if ( ch != '?' && ch != strArr[i] )
                {
                    return false; // Character mismatch
                }
            }
            return true; // String matches against pattern
        }

        if ( patIdxEnd == 0 )
        {
            return true; // Pattern contains only '*', which matches anything
        }

        // Process characters before first star
        while ( ( ch = patArr[patIdxStart] ) != '*' && strIdxStart <= strIdxEnd )
        {
            if ( ch != '?' && ch != strArr[strIdxStart] )
            {
                return false; // Character mismatch
            }
            patIdxStart++;
            strIdxStart++;
        }
        if ( strIdxStart > strIdxEnd )
        {
            // All characters in the string are used. Check if only '*'s are
            // left in the pattern. If so, we succeeded. Otherwise failure.
            for ( int i = patIdxStart; i <= patIdxEnd; i++ )
            {
                if ( patArr[i] != '*' )
                {
                    return false;
                }
            }
            return true;
        }

        // Process characters after last star
        while ( ( ch = patArr[patIdxEnd] ) != '*' && strIdxStart <= strIdxEnd )
        {
            if ( ch != '?' && ch != strArr[strIdxEnd] )
            {
                return false; // Character mismatch
            }
            patIdxEnd--;
            strIdxEnd--;
        }
        if ( strIdxStart > strIdxEnd )
        {
            // All characters in the string are used. Check if only '*'s are
            // left in the pattern. If so, we succeeded. Otherwise failure.
            for ( int i = patIdxStart; i <= patIdxEnd; i++ )
            {
                if ( patArr[i] != '*' )
                {
                    return false;
                }
            }
            return true;
        }

        // process pattern between stars. padIdxStart and patIdxEnd point
        // always to a '*'.
        while ( patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd )
        {
            int patIdxTmp = -1;
            for ( int i = patIdxStart + 1; i <= patIdxEnd; i++ )
            {
                if ( patArr[i] == '*' )
                {
                    patIdxTmp = i;
                    break;
                }
            }
            if ( patIdxTmp == patIdxStart + 1 )
            {
                // Two stars next to each other, skip the first one.
                patIdxStart++;
                continue;
            }
            // Find the pattern between padIdxStart & padIdxTmp in str between
            // strIdxStart & strIdxEnd
            int patLength = ( patIdxTmp - patIdxStart - 1 );
            int strLength = ( strIdxEnd - strIdxStart + 1 );
            int foundIdx = -1;
            strLoop: for ( int i = 0; i <= strLength - patLength; i++ )
            {
                for ( int j = 0; j < patLength; j++ )
                {
                    ch = patArr[patIdxStart + j + 1];
                    if ( ch != '?' && ch != strArr[strIdxStart + i + j] )
                    {
                        continue strLoop;
                    }
                }

                foundIdx = strIdxStart + i;
                break;
            }

            if ( foundIdx == -1 )
            {
                return false;
            }

            patIdxStart = patIdxTmp;
            strIdxStart = foundIdx + patLength;
        }

        // All characters in the string are used. Check if only '*'s are left
        // in the pattern. If so, we succeeded. Otherwise failure.
        for ( int i = patIdxStart; i <= patIdxEnd; i++ )
        {
            if ( patArr[i] != '*' )
            {
                return false;
            }
        }
        return true;
    }

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

    static Pattern compile( String pattern )
    {
        if ( pattern.startsWith( "!" ) )
        {
            return new NegativePattern( pattern, compile( pattern.substring( 1 ) ) );
        }
        else
        {
            char[][] stokens = tokenizeAndSplit( pattern );
            char[][] tokens = new char[ stokens.length ][];
            for ( int i = 0; i < stokens.length; i++ )
            {
                tokens[i] = anyOrChars( stokens[i] );
            }
            if ( tokens.length > 5 )
            {
                throw new IllegalArgumentException( "Invalid pattern: " + pattern );
            }
            //
            // Check the tokens and build an appropriate Pattern
            // Special care needs to be taken if the first or the last part is '*'
            // because this allows the '*' to match multiple tokens
            //
            if ( tokens.length == 1 )
            {
                if ( tokens[0] == ANY )
                {
                    // *
                    return all( pattern );
                }
                else
                {
                    // [pat0]
                    return match( pattern, tokens[0], 0 );
                }
            }
            if ( tokens.length == 2 )
            {
                if ( tokens[0] == ANY )
                {
                    if ( tokens[1] == ANY )
                    {
                        // *:*
                        return all( pattern );
                    }
                    else
                    {
                        // *:[pat1]
                        return match( pattern, tokens[1], 0, 3 );
                    }
                }
                else
                {
                    if ( tokens[1] == ANY )
                    {
                        // [pat0]:*
                        return match( pattern, tokens[0], 0 );
                    }
                    else
                    {
                        // [pat0]:[pat1]
                        Pattern m00 = match( tokens[0], 0 );
                        Pattern m11 = match( tokens[1], 1 );
                        return and( pattern, m00, m11 );
                    }
                }
            }
            if ( tokens.length == 3 )
            {
                if ( tokens[0] == ANY )
                {
                    if ( tokens[1] == ANY )
                    {
                        if ( tokens[2] == ANY )
                        {
                            // *:*:*
                            return all( pattern );
                        }
                        else
                        {
                            // *:*:[pat2]
                            return match( pattern, tokens[2], 2, 3 );
                        }
                    }
                    else
                    {
                        if ( tokens[2] == ANY )
                        {
                            // *:[pat1]:*
                            return match( pattern, tokens[1], 1, 2 );
                        }
                        else
                        {
                            // *:[pat1]:[pat2]
                            Pattern m11 = match( tokens[1], 1 );
                            Pattern m12 = match( tokens[1], 2 );
                            Pattern m22 = match( tokens[2], 2 );
                            Pattern m23 = match( tokens[2], 3 );
                            return or( pattern, and( m11, m22 ), and( m12, m23 ) );
                        }
                    }
                }
                else
                {
                    if ( tokens[1] == ANY )
                    {
                        if ( tokens[2] == ANY )
                        {
                            // [pat0]:*:*
                            return match( pattern, tokens[0], 0, 1 );
                        }
                        else
                        {
                            // [pat0]:*:[pat2]
                            Pattern m00 = match( tokens[0], 0 );
                            Pattern m223 = match( tokens[2], 2, 3 );
                            return and( pattern, m00, m223 );
                        }
                    }
                    else
                    {
                        if ( tokens[2] == ANY )
                        {
                            // [pat0]:[pat1]:*
                            Pattern m00 = match( tokens[0], 0 );
                            Pattern m11 = match( tokens[1], 1 );
                            return and( pattern, m00, m11 );
                        }
                        else
                        {
                            // [pat0]:[pat1]:[pat2]
                            Pattern m00 = match( tokens[0], 0 );
                            Pattern m11 = match( tokens[1], 1 );
                            Pattern m22 = match( tokens[2], 2 );
                            return and( pattern, m00, m11, m22 );
                        }
                    }
                }
            }
            if ( tokens.length >= 4 )
            {
                List<Pattern> patterns = new ArrayList<>();
                for ( int i = 0; i < tokens.length; i++ )
                {
                    if ( tokens[i] != ANY )
                    {
                        patterns.add( match( tokens[i], i ) );
                    }
                }
                return and( pattern, patterns.toArray( new Pattern[0] ) );
            }
            throw new IllegalStateException();
        }
    }

    /** Creates a positional matching pattern */
    private static Pattern match( String pattern, char[] token, int posVal )
    {
        return match( pattern, token, posVal, posVal );
    }

    /** Creates a positional matching pattern */
    private static Pattern match( char[] token, int posVal )
    {
        return match( "", token, posVal, posVal );
    }

    /** Creates a positional matching pattern */
    private static Pattern match( String pattern, char[] token, int posMin, int posMax )
    {
        boolean hasWildcard = false;
        for ( char ch : token )
        {
            if ( ch == '*' || ch == '?' )
            {
                hasWildcard = true;
                break;
            }
        }
        if ( hasWildcard || posMax == 4 )
        {
            return new PosPattern( pattern, token, posMin, posMax );
        }
        else
        {
            return new EqPattern( pattern, token, posMin, posMax );
        }
    }

    /** Creates a positional matching pattern */
    private static Pattern match( char[] token, int posMin, int posMax )
    {
        return new PosPattern( "", token, posMin, posMax );
    }

    /** Creates an AND pattern */
    private static Pattern and( String pattern, Pattern... patterns )
    {
        return new AndPattern( pattern, patterns );
    }

    /** Creates an AND pattern */
    private static Pattern and( Pattern... patterns )
    {
        return and( "", patterns );
    }

    /** Creates an OR pattern */
    private static Pattern or( String pattern, Pattern... patterns )
    {
        return new OrPattern( pattern, patterns );
    }

    /** Creates an OR pattern */
    private static Pattern or( Pattern... patterns )
    {
        return or( "", patterns );
    }

    /** Creates a match-all pattern */
    private static Pattern all( String pattern )
    {
        return new MatchAllPattern( pattern );
    }

    /**
     * Abstract class for patterns
     */
    abstract static class Pattern
    {
        private final String pattern;

        Pattern( String pattern )
        {
            this.pattern = Objects.requireNonNull( pattern );
        }

        public abstract boolean matches( char[][] parts );

        /**
         * Returns a string containing a fixed artifact gatv coordinates
         * or null if the pattern can not be translated.
         */
        public String translateEquals()
        {
            return null;
        }

        /**
         * Check if the this pattern is a fixed pattern on the specified pos.
         */
        protected String translateEquals( int pos )
        {
            return null;
        }

        @Override
        public String toString()
        {
            return pattern;
        }
    }

    /**
     * Simple pattern which performs a logical AND between one or more patterns.
     */
    static class AndPattern extends Pattern
    {
        private final Pattern[] patterns;

        AndPattern( String pattern, Pattern[] patterns )
        {
            super( pattern );
            this.patterns = patterns;
        }

        @Override
        public boolean matches( char[][] parts )
        {
            for ( Pattern pattern : patterns )
            {
                if ( !pattern.matches( parts ) )
                {
                    return false;
                }
            }
            return true;
        }

        @Override
        public String translateEquals()
        {
            String[] strings = new String[ patterns.length ];
            for ( int i = 0; i < patterns.length; i++ )
            {
                strings[i] = patterns[i].translateEquals( i );
                if ( strings[i] == null )
                {
                    return null;
                }
            }
            StringBuilder sb = new StringBuilder();
            for ( int i = 0; i < strings.length; i++ )
            {
                if ( i > 0 )
                {
                    sb.append( ":" );
                }
                sb.append( strings[i] );
            }
            return sb.toString();
        }
    }

    /**
     * Simple pattern which performs a logical OR between one or more patterns.
     */
    static class OrPattern extends Pattern
    {
        private final Pattern[] patterns;

        OrPattern( String pattern, Pattern[] patterns )
        {
            super( pattern );
            this.patterns = patterns;
        }

        @Override
        public boolean matches( char[][] parts )
        {
            for ( Pattern pattern : patterns )
            {
                if ( pattern.matches( parts ) )
                {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * A positional matching pattern, to check if a token in the gatv coordinates
     * having a position between posMin and posMax (both inclusives) can match
     * the pattern.
     */
    static class PosPattern extends Pattern
    {
        private final char[] patternCharArray;
        private final int posMin;
        private final int posMax;

        PosPattern( String pattern, char[] patternCharArray, int posMin, int posMax )
        {
            super( pattern );
            this.patternCharArray = patternCharArray;
            this.posMin = posMin;
            this.posMax = posMax;
        }

        @Override
        public boolean matches( char[][] parts )
        {
            for ( int i = posMin; i <= posMax; i++ )
            {
                if ( match( patternCharArray, parts[i], i == 4 ) )
                {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * Looks for an exact match in the gatv coordinates between
     * posMin and posMax (both inclusives)
     */
    static class EqPattern extends Pattern
    {
        private final char[] token;
        private final int posMin;
        private final int posMax;

        EqPattern( String pattern, char[] patternCharArray, int posMin, int posMax )
        {
            super( pattern );
            this.token = patternCharArray;
            this.posMin = posMin;
            this.posMax = posMax;
        }

        @Override
        public boolean matches( char[][] parts )
        {
            for ( int i = posMin; i <= posMax; i++ )
            {
                if ( Arrays.equals( token, parts[i] ) )
                {
                    return true;
                }
            }
            return false;
        }

        @Override
        public String translateEquals()
        {
            return translateEquals( 0 );
        }

        public String translateEquals( int pos )
        {
            return posMin == pos && posMax == pos
                && ( pos < 3 || ( token[0] != '[' && token[0] != '(' ) )
                    ? String.valueOf( token ) : null;
        }

    }

    /**
     * Matches all input
     */
    static class MatchAllPattern extends Pattern
    {
        MatchAllPattern( String pattern )
        {
            super( pattern );
        }

        @Override
        public boolean matches( char[][] parts )
        {
            return true;
        }
    }

    /**
     * Negative pattern
     */
    static class NegativePattern extends Pattern
    {
        private final Pattern inner;

        NegativePattern( String pattern, Pattern inner )
        {
            super( pattern );
            this.inner = inner;
        }

        @Override
        public boolean matches( char[][] parts )
        {
            return inner.matches( parts );
        }
    }

}
