/*
 *  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. 
 *  
 */
package org.apache.directory.studio.openldap.config.editor.wrappers;

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

import org.apache.directory.api.util.Strings;
import org.apache.directory.studio.common.ui.widgets.OrderedElement;
import org.apache.directory.studio.openldap.common.ui.model.DnSpecStyleEnum;
import org.apache.directory.studio.openldap.common.ui.model.DnSpecTypeEnum;
import org.apache.directory.studio.openldap.common.ui.model.LimitSelectorEnum;


/**
 * This class wraps the olcLimits parameter :
 * <pre>
 * olcLimits ::= selector limit limit-e
 * selector  ::= '*' | 'anonymous' | 'users' | dnspec '=' pattern | group '=' pattern
 * dnspec ::= 'dn' type-e style-e
 * type-e ::= '.self' | '.this' | e
 * style-e ::= '.exact' | '.base' | '.one' | '.onelevel' | '.sub' | '.subtree' | '.children' | '.regex' | '.anonymous' | e
 * pattern ::= '*' | '.*' | '"' REGEXP '"'
 * group ::= 'group' group-oc
 * group-oc ::= '/' OBJECT_CLASS group-at | e
 * group-at ::= '/' ATTRIBUTE_TYPE | e
 * limit ::= 'time' time-limit | 'size' size-limit
 * time-limit ::= '.soft=' limit-value| '.hard=' time-hard | '=' limit-value
 * time-hard ::= limit-value | 'soft'
 * size-limit ::= '.soft=' limit-value | '.hard=' size-hard | '.unchecked=' | '=' limit-value
 * size-hard ::= limit-value | 'soft' | 'disable'
 * limit-value ::= INT | 'unlimited'
 * </pre>
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class LimitsWrapper implements Cloneable, Comparable<LimitsWrapper>, OrderedElement
{
    /** Prefix, as the Limits are ordered */
    private int prefix;
    
    /** The selector */
    private LimitSelectorEnum selector;
    
    /** The pattern if the selector is a dnspec or a group */
    private String selectorPattern;
    
    /** The type if the selector is a DnSpec */ 
    private DnSpecTypeEnum dnSpecType;
    
    /** The style if the selector is a DnSpec */ 
    private DnSpecStyleEnum dnSpecStyle;
    
    /** The group ObjectClass */
    private String objectClass;
    
    /** The group AttributeType */
    private String attributeType;
    
    /** The list of limits, as Strings */
    private List<LimitWrapper> limits = new ArrayList<>();
    
    /** A flag to tell if the limits is valid or not */
    private boolean isValid = true;
    
    /** A flag used when the limit is invalid */
    private static final int ERROR = -1;

    /** A flag used when the parsing is completed */
    private static final int EOL = Integer.MIN_VALUE;
    
    /**
     * Create a LimitsrWrapper instance
     */
    public LimitsWrapper()
    {
    }
    
    
    /**
     * Create a LimitsrWrapper instance from a String
     * 
     * @param limitsStr The String that contain the value
     */
    public LimitsWrapper( String limitsStr )
    {
        if ( limitsStr != null )
        {
            // use a lowercase version of the string
            String lowerCaseLimits = limitsStr.toLowerCase();
            int pos = 0;
            
            // It's ordered : process the prefix
            if ( Strings.isCharASCII( lowerCaseLimits, pos, '{' ) )
            {
                pos++;
                prefix = 0;
                
                while ( pos < lowerCaseLimits.length() )
                {
                    char c = lowerCaseLimits.charAt( pos );
                    
                    if ( ( c >= '0' ) && ( c <= '9' ) )
                    {
                        prefix = prefix * 10 + ( c - '0' );
                        pos++;
                    }
                    else if ( c == '}' )
                    {
                        pos++;
                        break;
                    }
                    else
                    {
                        isValid = false;
                        break;
                    }
                }
            }
            
            if ( isValid )
            {
                lowerCaseLimits = lowerCaseLimits.substring( pos );
                // Process the selector
                if ( lowerCaseLimits.startsWith( LimitSelectorEnum.ANY.getName() ) )
                {
                    selector = LimitSelectorEnum.ANY;
                    pos += LimitSelectorEnum.ANY.getName().length();
                }
                if ( lowerCaseLimits.startsWith( LimitSelectorEnum.ANONYMOUS.getName() ) )
                {
                    selector = LimitSelectorEnum.ANONYMOUS;
                    pos += LimitSelectorEnum.ANONYMOUS.getName().length();
                }
                else if ( lowerCaseLimits.startsWith( LimitSelectorEnum.USERS.getName() ) )
                {
                    selector = LimitSelectorEnum.USERS;
                    pos += LimitSelectorEnum.USERS.getName().length();
                }
                else if ( lowerCaseLimits.startsWith( LimitSelectorEnum.DNSPEC.getName() ) )
                {
                    selector = LimitSelectorEnum.DNSPEC;
                    pos += LimitSelectorEnum.DNSPEC.getName().length();
                    
                    // parse the type
                    pos = parseDnSpec( lowerCaseLimits, pos );
                    
                    if ( pos == ERROR )
                    {
                        isValid = false;
                    }
                }
                else if ( lowerCaseLimits.startsWith( LimitSelectorEnum.GROUP.getName() ) )
                {
                    selector = LimitSelectorEnum.GROUP;
                    pos += LimitSelectorEnum.GROUP.getName().length();
                    
                    pos = parseGroup( lowerCaseLimits, pos );
                    
                    if ( pos == ERROR )
                    {
                        // This is an error
                        isValid = false;
                    }
                }
            }
            
            // Process the limits, only if the selector was valid
            if ( isValid )
            {
                boolean noLimit = true;
                
                while ( pos >= 0 )
                {
                    pos = parseLimit( lowerCaseLimits, pos );
                    
                    if ( noLimit )
                    {
                        if ( pos == EOL )
                        {
                            // We must have at least one limit
                            {
                                isValid = false;
                                break;
                            }
                        }
                        else
                        {
                            noLimit = false;
                        }
                    }
                
                    if ( pos == ERROR )
                    {
                        isValid = false;
                        break;
                    }
                }
            }
        }
    }
    
    
    /**
     * Parse the DNSpec part : 
     * <pre>
     * dnspec ::= 'dn' type-e style-e '=' pattern
     * type-e ::= '.self' | '.this' | e
     * style-e ::= '.exact' | '.base' | '.one' | '.onelevel' | '.sub' | '.subtree' | '.children' | '.regex' | '.anonymous' | e
     * </pre>
     */
    private int parseDnSpec( String str, int pos )
    {
        // The type
        if ( str.startsWith( ".self", pos ) )
        {
            dnSpecType = DnSpecTypeEnum.SELF;
            pos += 5;
        }
        else if ( str.startsWith( ".this", pos ) )
        {
            dnSpecType = DnSpecTypeEnum.THIS;
            pos += 5;
        }
        
        // The style
        if ( str.startsWith( ".exact", pos ) )
        {
            dnSpecStyle = DnSpecStyleEnum.EXACT;
            pos += 6;
        }
        else if ( str.startsWith( ".base", pos ) )
        {
            dnSpecStyle = DnSpecStyleEnum.BASE;
            pos += 5;
        }
        else if ( str.startsWith( ".onelevel", pos ) )
        {
            dnSpecStyle = DnSpecStyleEnum.ONE_LEVEL;
            pos += 9;
        }
        else if ( str.startsWith( ".one", pos ) )
        {
            dnSpecStyle = DnSpecStyleEnum.ONE_LEVEL;
            pos += 4;
        }
        else if ( str.startsWith( ".subtree", pos ) )
        {
            dnSpecStyle = DnSpecStyleEnum.SUBTREE;
            pos += 8;
        }
        else if ( str.startsWith( ".sub", pos ) )
        {
            dnSpecStyle = DnSpecStyleEnum.SUBTREE;
            pos += 4;
        }
        else if ( str.startsWith( ".children", pos ) )
        {
            dnSpecStyle = DnSpecStyleEnum.CHILDREN;
            pos += 9;
        }
        else if ( str.startsWith( ".regex", pos ) )
        {
            dnSpecStyle = DnSpecStyleEnum.REGEXP;
            pos += 6;
        }
        else if ( str.startsWith( ".anonymous", pos ) )
        {
            dnSpecStyle = DnSpecStyleEnum.ANONYMOUS;
            pos += 10;
        }
        
        // The pattern
        return parsePattern( str, pos );
    }

    
    /**
     * Parse the group part :
     * <pre>
     * group ::= 'group' group-oc '=' pattern
     * group-oc ::= '/' OBJECT_CLASS group-at | e
     * group-at ::= '/' ATTRIBUTE_TYPE | e
     * </pre>
     */
    private int parseGroup( String str, int pos )
    {
        // Check if we have an ObjectClass
        if ( Strings.isCharASCII( str, pos, '/' ) )
        {
            int i = pos + 1;
            
            for ( ; i < str.length(); i++ )
            {
                char c = str.charAt( i );
                
                if ( ( ( c >= 'a' ) && ( c <= 'z' ) ) || ( ( c >= 'A' ) && ( c <= 'Z' ) ) ||
                    ( ( c >= '0' ) && ( c <= '9' ) ) || ( c == '.' ) || ( c == '-' ) || ( c == '_' ) )
                {
                    continue;
                }
            }
            
            if ( i > pos + 1 )
            {
                // An ObjectClass
                objectClass = str.substring( pos + 1, i );
            }
            
            pos = i;
        }
        
        // Check if we have an AttributeType
        if ( Strings.isCharASCII( str, pos, '/' ) )
        {
            int i = pos + 1;
            
            for ( ; i < str.length(); i++ )
            {
                char c = str.charAt( i );
                
                if ( ( ( c >= 'a' ) && ( c <= 'z' ) ) || ( ( c >= 'A' ) && ( c <= 'Z' ) ) ||
                    ( ( c >= '0' ) && ( c <= '9' ) ) || ( c == '.' ) || ( c == '-' ) || ( c == '_' ) )
                {
                    continue;
                }
            }
            
            if ( i > pos + 1 )
            {
                // An AttributeType
                attributeType = str.substring( pos + 1, i );
            }
            
            pos = i;
        }
        
        
        // The pattern
        return parsePattern( str, pos );
    }

    
    /**
     * Search for a pattern
     */
    private int parsePattern( String str, int pos )
    {
        if ( !Strings.isCharASCII( str, pos, '=' ) )
        {
            return ERROR;
        }
        
        pos++;

        if ( !Strings.isCharASCII( str, pos, '"' ) )
        {
            return ERROR;
        }
        
        pos++;
        
        boolean escapeSeen = false;
        
        for ( int  i = pos; i < str.length(); i++ )
        {
            if ( str.charAt( i ) == '\\' )
            {
                escapeSeen = !escapeSeen;
            }
            else
            {
                if ( str.charAt( i ) == '"' )
                {
                    if ( escapeSeen )
                    {
                        escapeSeen = false;
                    }
                    else
                    {
                        // This is the end of the patter
                        selectorPattern = str.substring( pos, i );
                        return i + 1;
                    }
                }
                else
                {
                    escapeSeen = false;
                }
            }
        }
        
        // The final '"' has not been found, this is an error.
        return ERROR;
    }
    
    
    /**
     * Parses the limit.
     * <pre>
     * limit ::= 'time' time-limit | 'size' size-limit
     * time-limit ::= '.soft=' limit-value| '.hard=' time-hard | '=' limit-value
     * time-hard ::= limit-value | 'soft'
     * size-limit ::= '.soft=' limit-value | '.hard=' size-hard | '.unchecked=' | '=' limit-value
     * size-hard ::= limit-value | 'soft' | 'disable'
     * limit-value ::= INT | 'unlimited' | 'none'
     * </pre>
     */
    private int parseLimit( String str, int pos )
    {
        // Remove spaces
        while ( Strings.isCharASCII( str, pos, ' ' ) )
        {
            pos++;
        }
        
        String limitStr = str.substring( pos );
        
        if ( Strings.isEmpty( limitStr ) )
        {
            return EOL;
        }
        
        int i = 0;
        
        if ( limitStr.startsWith( "time" ) )
        {
            // fetch the time limit (everything that goes up to a space or the end of the string
            for ( ; i < limitStr.length(); i++ )
            {
                if ( limitStr.charAt( i ) == ' ' )
                {
                    break;
                }
            }
            
            if ( i == 0 )
            {
                return ERROR;
            }
            
            TimeLimitWrapper timeLimitWrapper = new TimeLimitWrapper( limitStr.substring( 0, i ) );
            
            if ( timeLimitWrapper.isValid() )
            {
                limits.add( timeLimitWrapper );
                return pos + i; 
            }
            else
            {
                return ERROR;
            }
        }
        else if ( limitStr.startsWith( "size" ) )
        {
            // fetch the size limit (everything that goes up to a space or the end of the string
            for ( ;i < limitStr.length(); i++ )
            {
                if ( limitStr.charAt( i ) == ' ' )
                {
                    break;
                }
            }
            
            if ( i == 0 )
            {
                return ERROR;
            }
            
            SizeLimitWrapper sizeLimitWrapper = new SizeLimitWrapper( limitStr.substring( 0, i ) );
            
            if ( sizeLimitWrapper.isValid() )
            {
                limits.add( sizeLimitWrapper );
                return pos + i; 
            }
            else
            {
                return ERROR;
            }
        }
        else
        {
            return ERROR;
        }
    }
    
    
    /**
     * Sets a new prefix
     * 
     * @param prefix the prefix to set
     */
    public void setPrefix( int prefix )
    {
        this.prefix = prefix;
    }

    
    /**
     * @return the prefix
     */
    public int getPrefix()
    {
        return prefix;
    }
    
    
    /**
     * {@inheritDoc}
     */
    public void decrementPrefix()
    {
        prefix--;
    }
    
    
    /**
     * {@inheritDoc}
     */
    public void incrementPrefix()
    {
        prefix++;
    }
    
    
    /**
     * @return the selector
     */
    public LimitSelectorEnum getSelector()
    {
        return selector;
    }


    /**
     * @param selector the selector to set
     */
    public void setSelector( LimitSelectorEnum selector )
    {
        this.selector = selector;
    }


    /**
     * @return the selectorPattern
     */
    public String getSelectorPattern()
    {
        return selectorPattern;
    }


    /**
     * @param selectorPattern the selectorPattern to set
     */
    public void setSelectorPattern( String selectorPattern )
    {
        this.selectorPattern = selectorPattern;
    }


    /**
     * @return the dnSpecType
     */
    public DnSpecTypeEnum getDnSpecType()
    {
        return dnSpecType;
    }


    /**
     * @param dnSpecType the dnSpecType to set
     */
    public void setDnSpecType( DnSpecTypeEnum dnSpecType )
    {
        this.dnSpecType = dnSpecType;
    }


    /**
     * @return the dnSpecStyle
     */
    public DnSpecStyleEnum getDnSpecStyle()
    {
        return dnSpecStyle;
    }


    /**
     * @param dnSpecStyle the dnSpecStyle to set
     */
    public void setDnSpecStyle( DnSpecStyleEnum dnSpecStyle )
    {
        this.dnSpecStyle = dnSpecStyle;
    }


    /**
     * @return the objectClass
     */
    public String getObjectClass()
    {
        return objectClass;
    }


    /**
     * @param objectClass the objectClass to set
     */
    public void setObjectClass( String objectClass )
    {
        this.objectClass = objectClass;
    }


    /**
     * @return the attributeType
     */
    public String getAttributeType()
    {
        return attributeType;
    }


    /**
     * @param attributeType the attributeType to set
     */
    public void setAttributeType( String attributeType )
    {
        this.attributeType = attributeType;
    }


    /**
     * @return the limits
     */
    public List<LimitWrapper> getLimits()
    {
        return limits;
    }


    /**
     * @param limits the limits to set
     */
    public void setLimits( List<LimitWrapper> limits )
    {
        this.limits = limits;
    }


    /**
     * Tells if the Limits element is valid or not
     * @return true if the values are correct, false otherwise
     */
    public boolean isValid()
    {
        return isValid;
    }
    
    
    /**
     * Clone the current object
     */
    public LimitsWrapper clone()
    {
        try
        {
            return (LimitsWrapper)super.clone();
        }
        catch ( CloneNotSupportedException e )
        {
            return null;
        }
    }
    
    
    /**
     * LimitsWrapper are ordered objects
     * @see Object#equals(Object)
     */
    public boolean equals( Object that )
    {
        // Quick test
        if ( this == that )
        {
            return true;
        }
        
        if ( that instanceof LimitsWrapper )
        {
            LimitsWrapper thatInstance = (LimitsWrapper)that;
            
            // Check the prefix first
            if ( prefix != thatInstance.prefix )
            {
                return false;
            }

            // The selector
            if ( selector != thatInstance.selector )
            {
                return false;
            }
            
            // Same selector. Depending on the type, check the two instance
            switch ( selector )
            {
                case DNSPEC :
                    if ( ( dnSpecStyle != thatInstance.dnSpecStyle ) || ( dnSpecType != thatInstance.dnSpecType ) )
                    {
                        return false;
                    }

                    // Check the pattern
                    if ( selectorPattern != thatInstance.selectorPattern )
                    {
                        return false;
                    }
                    
                    break;
                    
                case GROUP :
                    // If we have an ObjectClass, check it
                    if ( ( objectClass != null ) && ( !objectClass.equals( thatInstance.objectClass ) ) )
                    {
                        return false;
                    }
                    else if ( thatInstance.objectClass != null )
                    {
                        return false;
                    }

                    // If we have an AttributeType, check it
                    if ( ( attributeType != null ) && ( !attributeType.equals( thatInstance.attributeType ) ) )
                    {
                        return false;
                    }
                    else if ( thatInstance.attributeType != null )
                    {
                        return false;
                    }

                    // Check the pattern
                    if ( selectorPattern != thatInstance.selectorPattern )
                    {
                        return false;
                    }
                    
                    break;
                case ANY :
                case ANONYMOUS :
                case USERS :
                    break;
            }
            
            // Check the limits now
            if ( limits.size() != thatInstance.limits.size() )
            {
                return false;
            }
            
            // Iterate on both limits (they are not ordered... This is a O(n2) loop.
            for ( LimitWrapper limit : limits )
            {
                boolean found = false;
                
                for ( LimitWrapper thatLimit : thatInstance.limits )
                {
                    if ( limit.equals( thatLimit ) )
                    {
                        found = true;
                        break;
                    }
                }
                
                if ( !found )
                {
                    return false;
                }
            }
            
            return true;
        }
        else
        {
            return false;
        }
    }
    
    
    /**
     * @see Object#hashCode()
     */
    public int hashCode()
    {
        int h = 37;
        
        h += h*17 + selector.hashCode();
        
        // The selector
        switch ( selector )
        {
            case DNSPEC :
                if ( dnSpecType != null )
                {
                    h += h*17 + dnSpecType.hashCode();
                }
                
                if ( dnSpecStyle != null )
                {
                    h += h*17 + dnSpecStyle.hashCode();
                }
                
                break;
                
            case GROUP :
                if ( selectorPattern != null )
                {
                    h += h*17 + selectorPattern.hashCode();
                }
                
                break;
        }
        
        // The limits
        for ( LimitWrapper limit : limits )
        {
            h += h*17 + limit.hashCode();
        }
        
        return h;
    }


    /**
     * @see Comparable#compareTo()
     */
    public int compareTo( LimitsWrapper that )
    {
        if ( that == null )
        {
            return 1;
        }
        
        // Check the prefix
        if ( prefix < that.prefix )
        {
            return -1;
        }
        else if ( prefix > that.prefix )
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }

    
    /**
     * @see Object#toString()
     */
    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        
        sb.append( '{' ).append( prefix ).append( '}' );
        sb.append( selector.getName() );
        
        // The selector
        switch ( selector )
        {
            case ANY :
            case ANONYMOUS :
            case USERS :
                break;
                
            case DNSPEC :
                if ( dnSpecType != null )
                {
                    sb.append( '.' ).append( dnSpecType.getName() );
                }
                
                if ( dnSpecStyle != null )
                {
                    sb.append( '.' ).append( dnSpecStyle.getName() );
                }

                sb.append( "=\"" );
                sb.append( selectorPattern );
                sb.append(  '\"' );
                
                break;
                
            case GROUP :
                if ( objectClass != null )
                {
                    sb.append( '/' ).append( objectClass );
                }
                
                if ( attributeType != null )
                {
                    sb.append( '/' ).append( attributeType );
                }
                
                sb.append( "=\"" );
                sb.append( selectorPattern );
                sb.append(  '\"' );
                
                break;
        }
        
        // The limits
        for ( LimitWrapper limit : limits )
        {
            sb.append( ' ' );
            sb.append( limit );
        }
        
        return sb.toString();
    }
}
