/******************************************************************************
 * Copyright (c) 2010 Oracle
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 *    Konstantin Komissarchik - initial implementation and ongoing maintenance
 ******************************************************************************/

package org.eclipse.wst.common.project.facet.core.util.internal;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.osgi.util.NLS;
import org.eclipse.wst.common.project.facet.core.IVersion;
import org.eclipse.wst.common.project.facet.core.IVersionExpr;
import org.eclipse.wst.common.project.facet.core.internal.FacetCorePlugin;
import org.eclipse.wst.common.project.facet.core.internal.ProjectFacet;

/**
 * @author <a href="mailto:konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a>
 */

public final class VersionExpr<T extends IVersion>

    implements IVersionExpr
    
{
    private static final int SM1_START = 0;
    private static final int SM1_PARSING_START_VERSION = 1;
    private static final int SM1_PARSING_END_VERSION = 2;
    private static final int SM1_FINISHED_RANGE_INCLUSIVE = 3;
    private static final int SM1_FINISHED_RANGE_EXCLUSIVE = 4;
    private static final int SM1_PARSING_WILDCARD = 5;
    
    private static final int SM2_VERSION_START = 0;
    private static final int SM2_VERSION_CONTINUING = 1;
    private static final int SM2_ESCAPE = 2;
    
    private final Versionable<T> versionable;
    private final String originalExprString;
    private final List<ISubExpr> subexprs;
    private final String usedInPlugin;
    
    @SuppressWarnings( "unchecked" )
    public VersionExpr( final Object versionable,
                        final String expr,
                        final String usedInPlugin )
    
        throws CoreException
        
    {
        this( (Versionable<T>) versionable, expr, usedInPlugin );
    }
    
    public VersionExpr( final Versionable<T> versionable,
                        final String expr,
                        final String usedInPlugin )
    
        throws CoreException
        
    {
        this.versionable = versionable;
        this.originalExprString = expr;
        this.subexprs = new ArrayList<ISubExpr>();
        this.usedInPlugin = usedInPlugin;
        
        int state = SM1_START;
        Range range = null;
        boolean usingDeprecatedSyntax = false;
        
        for( MutableInteger position = new MutableInteger(); 
             position.value < expr.length(); position.value++ )
        {
            final char ch = expr.charAt( position.value );
            
            switch( state )
            {
                case SM1_START:
                {
                    if( ch == '[' )
                    {
                        range = new Range();
                        range.includesStartVersion = true;
                        state = SM1_PARSING_START_VERSION;
                    }
                    else if( ch == '(' )
                    {
                        range = new Range();
                        range.includesStartVersion = false;
                        state = SM1_PARSING_START_VERSION;
                    }
                    else if( ch == '*' )
                    {
                        this.subexprs.add( new Wildcard() );
                        state = SM1_PARSING_WILDCARD;
                    }
                    else if( ch == ' ' || ch == ',' )
                    {
                        // ignore
                    }
                    else
                    {
                        final StringBuffer buf = new StringBuffer();
                        final int exitState = parseVersion( expr, position, buf );
                        
                        if( exitState == SM1_START )
                        {
                            final String vstr = buf.toString();
                            final Range r = new Range();
                            
                            if( vstr.startsWith( ">=" ) ) //$NON-NLS-1$
                            {
                                r.startVersion = parseVersion( vstr.substring( 2 ) );
                                r.includesStartVersion = true;
                                usingDeprecatedSyntax = true;
                            }
                            else if( vstr.startsWith( ">" ) ) //$NON-NLS-1$
                            {
                                r.startVersion = parseVersion( vstr.substring( 1 ) );
                                r.includesStartVersion = false;
                                usingDeprecatedSyntax = true;
                            }
                            else if( vstr.startsWith( "<=" ) ) //$NON-NLS-1$
                            {
                                r.endVersion = parseVersion( vstr.substring( 2 ) );
                                r.includesEndVersion = true;
                                usingDeprecatedSyntax = true;
                            }
                            else if( vstr.startsWith( "<" ) ) //$NON-NLS-1$
                            {
                                r.endVersion = parseVersion( vstr.substring( 1 ) );
                                r.includesEndVersion = false;
                                usingDeprecatedSyntax = true;
                            }
                            else
                            {
                                r.startVersion = parseVersion( vstr );
                                r.includesStartVersion = true;
                                r.endVersion = r.startVersion;
                                r.includesEndVersion = true;
                            }
                            
                            this.subexprs.add( r );
                        }
                        else if( exitState == SM1_FINISHED_RANGE_INCLUSIVE ||
                                 exitState == SM1_FINISHED_RANGE_EXCLUSIVE )
                        {
                            range = new Range();
                            range.endVersion = parseVersion( buf.toString() );
                            state = exitState;
                            position.value--;
                        }
                        else
                        {
                            throw createInvalidVersionExprException( expr );
                        }
                    }
                    
                    break;
                }
                case SM1_PARSING_START_VERSION:
                {
                    final StringBuffer buf = new StringBuffer();
                    final int exitState = parseVersion( expr, position, buf );

                    if( exitState == SM1_START )
                    {
                        range.startVersion = parseVersion( buf.toString() );
                        this.subexprs.add( range );
                        range = null;
                        state = exitState;
                    }
                    else if( exitState == SM1_PARSING_END_VERSION )
                    {
                        range.startVersion = parseVersion( buf.toString() );
                        state = exitState;
                    }
                    else
                    {
                        throw createInvalidVersionExprException( expr );
                    }
                    
                    break;
                }
                case SM1_PARSING_END_VERSION:
                {
                    final StringBuffer buf = new StringBuffer();
                    final int exitState = parseVersion( expr, position, buf );
                    
                    if( exitState == SM1_FINISHED_RANGE_INCLUSIVE ||
                        exitState == SM1_FINISHED_RANGE_EXCLUSIVE )
                    {
                        range.endVersion = parseVersion( buf.toString() );
                        state = exitState;
                        position.value--;
                    }
                    else
                    {
                        throw createInvalidVersionExprException( expr );
                    }
                    
                    break;
                }
                case SM1_FINISHED_RANGE_INCLUSIVE:
                case SM1_FINISHED_RANGE_EXCLUSIVE:
                {
                    range.includesEndVersion 
                        = ( state == SM1_FINISHED_RANGE_INCLUSIVE );
                    
                    this.subexprs.add( range );
                    range = null;
                    
                    state = SM1_START;
                    
                    break;
                }
                case SM1_PARSING_WILDCARD:
                {
                    if( ch == ' ' )
                    {
                        // ignore
                    }
                    else if( ch == ',' )
                    {
                        state = SM1_START;
                    }
                    else
                    {
                        throw createInvalidVersionExprException( expr );
                    }
                }
                default:
                {
                    throw new IllegalStateException();
                }
            }
        }
        
        // Report the use of deprecated syntax.
        
        if( usingDeprecatedSyntax )
        {
            final StringBuilder msg = new StringBuilder();
            final String nl = System.getProperty( "line.separator" ); //$NON-NLS-1$
            
            msg.append( Resources.depMessage );
            msg.append( nl );
            msg.append( nl );
            msg.append( NLS.bind( Resources.depExpression, this.originalExprString ) );
            msg.append( nl );
            
            if( this.versionable instanceof ProjectFacet )
            {
                msg.append( NLS.bind( Resources.depUsedWithFacet, this.versionable.toString() ) );
            }
            else
            {
                msg.append( NLS.bind( Resources.depUsedWithRuntimeComponentType, this.versionable.toString() ) );
            }
            
            if( this.usedInPlugin != null )
            {
                msg.append( nl );
                msg.append( NLS.bind( Resources.depUsedInBundle, this.usedInPlugin ) );
            }
            
            FacetCorePlugin.logWarning( msg.toString(), true );
        }
    }
    
    private int parseVersion( final String str,
                              final MutableInteger position,
                              final StringBuffer version )
    
        throws CoreException
        
    {
        int localState = SM2_VERSION_START;
        int exitState = -1;
        
        for( ; exitState == -1 && position.value < str.length(); 
             position.value++ )
        {
            final char ch = str.charAt( position.value );
            
            switch( localState )
            {
                case SM2_VERSION_START:
                {
                    if( ch == '[' || ch == '(' || ch == ']' || ch == ')' ||
                        ch == '-' || ch == ',' )
                    {
                        throw createInvalidVersionExprException( str );
                    }
                    else if( ch == '\\' )
                    {
                        localState = SM2_ESCAPE;
                    }
                    else if( ch == ' ' )
                    {
                        // ignore
                    }
                    else
                    {
                        version.append( ch );
                        localState = SM2_VERSION_CONTINUING;
                    }
                    
                    break;
                }
                case SM2_VERSION_CONTINUING:
                {
                    if( ch == '[' || ch == '(' )
                    {
                        throw createInvalidVersionExprException( str );
                    }
                    else if( ch == '\\' )
                    {
                        localState = SM2_ESCAPE;
                    }
                    else if( ch == ',' )
                    {
                        exitState = SM1_START;
                    }
                    else if( ch == '-' )
                    {
                        exitState = SM1_PARSING_END_VERSION;
                    }
                    else if( ch == ']' )
                    {
                        exitState = SM1_FINISHED_RANGE_INCLUSIVE;
                    }
                    else if( ch == ')' )
                    {
                        exitState = SM1_FINISHED_RANGE_EXCLUSIVE;
                    }
                    else if( ch == ' ' )
                    {
                        // ignore
                    }
                    else
                    {
                        version.append( ch );
                    }
                    
                    break;
                }
                case SM2_ESCAPE:
                {
                    version.append( ch );
                    break;
                }
                default:
                {
                    throw new IllegalStateException();
                }
            }
        }
        
        position.value--;
        
        if( exitState != -1 )
        {
            return exitState;
        }
        else
        {
            if( localState == SM2_VERSION_CONTINUING )
            {
                // Expected end of input.
                
                return SM1_START;
            }
            else
            {
                // Unexpected end of input.
                
                throw createInvalidVersionExprException( str );
            }
        }
    }
    
    private IVersion parseVersion( final String str )
    {
        if( this.versionable.hasVersion( str ) )
        {
            return this.versionable.getVersion( str );
        }
        else
        {
            return new UnknownVersion<T>( this.versionable, str );
        }
    }
    
    public boolean check( final IVersion ver )
    {
        for( ISubExpr subexpr : this.subexprs )
        {
            if( subexpr.evaluate( ver ) )
            {
                return true;
            }
        }
        
        return false;
    }
    
    public boolean evaluate( final String ver )
    {
        return check( parseVersion( ver ) );
    }
    
    public boolean isSingleVersionMatch()
    {
        if( this.subexprs.size() == 1 )
        {
            final ISubExpr subExpr = this.subexprs.get( 0 );
            
            if( subExpr instanceof Range )
            {
                final Range range = (Range) subExpr;
                
                if( range.startVersion.equals( range.endVersion ) &&
                    range.includesStartVersion == range.includesEndVersion == true )
                {
                    return true;
                }
            }
        }
        
        return false;
    }

    public boolean isSimpleAllowNewer()
    {
        if( this.subexprs.size() == 1 )
        {
            final ISubExpr subExpr = this.subexprs.get( 0 );
            
            if( subExpr instanceof Range )
            {
                final Range range = (Range) subExpr;
            
                if( range.startVersion != null && range.endVersion == null &&
                    range.includesStartVersion  )
                {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    public boolean isWildcard()
    {
        return this.subexprs.size() == 1 && 
               this.subexprs.get( 0 ) instanceof Wildcard;
    }
    
    public String getFirstVersion()
    {
        if( isSingleVersionMatch() || isSimpleAllowNewer() )
        {
            final Range range = (Range) this.subexprs.get( 0 );
            return range.startVersion.getVersionString();
        }
        else
        {
            throw new IllegalStateException();
        }
    }
    
    public String toString()
    {
        final StringBuffer buf = new StringBuffer();
        
        for( ISubExpr subexpr : this.subexprs )
        {
            if( buf.length() > 0 ) buf.append( ',' );
            buf.append( subexpr.toString() );
        }
        
        return buf.toString();
    }
    
    public String toDisplayString()
    {
        if( this.subexprs.size() == 1 )
        {
            final ISubExpr subexpr = this.subexprs.get( 0 );
            
            if( subexpr instanceof Range )
            {
                final Range r = (Range) subexpr;
                
                if( r.isSingleVersion() )
                {
                    return r.startVersion.getVersionString();
                }
                else if( r.endVersion == null && r.includesStartVersion )
                {
                    return NLS.bind( Resources.versionOrNewer, 
                                     r.startVersion.getVersionString() );
                }
            }
        }

        boolean onlySingleVersions = true;
        
        for( ISubExpr subexpr : this.subexprs )
        {
            if( ! ( subexpr instanceof Range ) || 
                ! ( (Range) subexpr ).isSingleVersion() )
            {
                onlySingleVersions = false;
                break;
            }
        }
        
        if( onlySingleVersions )
        {
            final StringBuffer buf = new StringBuffer();

            for( Iterator<ISubExpr> itr = this.subexprs.iterator(); itr.hasNext(); )
            {
                final Range r = (Range) itr.next();
                
                if( buf.length() > 0 )
                {
                    if( itr.hasNext() )
                    {
                        buf.append( ", " ); //$NON-NLS-1$
                    }
                    else
                    {
                        buf.append( " or " ); //$NON-NLS-1$
                    }
                }
                
                buf.append( r.startVersion.getVersionString() );
            }
            
            return buf.toString();
        }
        
        return toString();
    }
    
    private static CoreException createInvalidVersionExprException( final String str )
    {
        final String msg
            = NLS.bind( Resources.invalidVersionExpr, str );
    
        final IStatus st = FacetCorePlugin.createErrorStatus( msg );
    
        return new CoreException( st );
    }
    
    private static interface ISubExpr
    {
        boolean evaluate( IVersion version );
    }
    
    private static final class Range
    
        implements ISubExpr
        
    {
        public IVersion startVersion = null;
        public boolean includesStartVersion = false;
        public IVersion endVersion = null;
        public boolean includesEndVersion = false;
        
        public boolean isSingleVersion()
        {
            return this.startVersion.equals( this.endVersion ) &&
                   this.includesStartVersion == this.includesEndVersion == true;
        }
        
        @SuppressWarnings( "unchecked" )
        public boolean evaluate( final IVersion version )
        {
            if( this.startVersion != null )
            {
                final int res = version.compareTo( this.startVersion );
                
                if( ! ( res > 0 || ( res == 0 && this.includesStartVersion ) ) )
                {
                    return false;
                }
            }
            
            if( this.endVersion != null )
            {
                final int res = version.compareTo( this.endVersion );
                
                if( ! ( res < 0 || ( res == 0 && this.includesEndVersion ) ) )
                {
                    return false;
                }
            }
            
            return true;
        }
        
        public String toString()
        {
            if( this.startVersion.equals( this.endVersion ) &&
                this.includesStartVersion == this.includesEndVersion == true )
            {
                return this.startVersion.getVersionString();
            }
            else
            {
                final StringBuffer buf = new StringBuffer();
                
                if( this.startVersion != null )
                {
                    buf.append( this.includesStartVersion ? '[' : '(' );
                    buf.append( this.startVersion.getVersionString() );
                }
                
                if( this.endVersion != null )
                {
                    if( buf.length() != 0 )
                    {
                        buf.append( '-' );
                    }
                    
                    buf.append( this.endVersion.getVersionString() );
                    buf.append( this.includesEndVersion ? ']' : ')' );
                }
                
                return buf.toString();
            }
        }
    }
    
    private static final class Wildcard
    
        implements ISubExpr
        
    {
        public boolean evaluate( final IVersion version )
        {
            return true;
        }
        
        public String toString()
        {
            return IVersionExpr.WILDCARD_SYMBOL;
        }
    }
    
    private static final class MutableInteger
    {
        public int value = 0;
    }
    
    private static final class Resources
    
        extends NLS
        
    {
        public static String invalidVersionExpr;
        public static String depMessage;
        public static String depExpression;
        public static String depUsedInBundle;
        public static String depUsedWithFacet;
        public static String depUsedWithRuntimeComponentType;
        public static String versionOrNewer;
        
        static
        {
            initializeMessages( VersionExpr.class.getName(), Resources.class );
        }
    }

}
