/*
 *  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.model.overlay;


import java.text.ParseException;

import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.util.Position;
import org.apache.directory.api.util.Strings;


/**
 * This class represents 'OlcValSortOverlay' value.
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class OlcValSortValue
{
    /** The 'weighted' constant string */
    private static final String WEIGHTED_STRING = "weighted";

    /** The attribute */
    private String attribute;

    /** The base DN */
    private Dn baseDn;

    /** The sort method */
    private OlcValSortMethodEnum sortMethod;

    private boolean isWeighted = false;


    /**
     * Gets the attribute.
     *
     * @return the attribute
     */
    public String getAttribute()
    {
        return attribute;
    }


    /**
     * Gets the base DN.
     *
     * @return the base DN
     */
    public Dn getBaseDn()
    {
        return baseDn;
    }


    /**
     * Gets the sort method.
     *
     * @return the sort method
     */
    public OlcValSortMethodEnum getSortMethod()
    {
        return sortMethod;
    }


    /**
     * Return whether or the selected sort method is weighted.
     *
     * @return <code>true</code> if the sort method is weighted,
     *         <code>false</code> if not
     */
    public boolean isWeighted()
    {
        return isWeighted;
    }


    /**
     * Sets the attribute.
     *
     * @param attribute the attribute
     */
    public void setAttribute( String attribute )
    {
        this.attribute = attribute;
    }


    /**
     * Sets the base DN.
     *
     * @param baseDn the base DN
     */
    public void setBaseDn( Dn baseDn )
    {
        this.baseDn = baseDn;
    }


    /**
     * Sets the sort method.
     *
     * @param sortMethod the sort method
     */
    public void setSortMethod( OlcValSortMethodEnum sortMethod )
    {
        this.sortMethod = sortMethod;
    }


    /**
     * Sets whether or the selected sort method is weighted..
     *
     * @param isWeighted the value
     */
    public void setWeighted( boolean isWeighted )
    {
        this.isWeighted = isWeighted;
    }


    /**
     * {@inheritDoc}
     */
    public String toString()
    {
        StringBuilder sb = new StringBuilder();

        // Attribute
        sb.append( attribute );
        sb.append( ' ' );

        // Base DN
        boolean baseDnNeedsEscaping = false;

        if ( baseDn != null )
        {
            baseDnNeedsEscaping = needsEscaping( baseDn.toString() );
        }

        if ( baseDnNeedsEscaping )
        {
            sb.append( '"' );
        }

        sb.append( baseDn );

        if ( baseDnNeedsEscaping )
        {
            sb.append( '"' );
        }

        sb.append( ' ' );

        // Weighted
        if ( isWeighted )
        {
            sb.append( WEIGHTED_STRING );

            // Sort method
            if ( sortMethod != null )
            {
                // Sort method
                sb.append( ' ' );
                sb.append( sortMethod );
            }
        }
        else
        {
            // Sort method
            sb.append( sortMethod );
        }

        return sb.toString();
    }


    /**
     * Indicates if the given string needs escaping.
     *
     * @param s the string
     * @return <code>true</code> if the given string needs escaping
     *         <code>false</code> if not.
     */
    private boolean needsEscaping( String s )
    {
        if ( s != null )
        {
            return s.contains( " " );
        }

        return false;
    }


    /**
     * Parses a OlcValSortValue value.
     *
     * @param s the string to be parsed
     * @return the associated OlcValSortValue object
     * @throws ParseException if there are any recognition errors (bad syntax)
     */
    public static synchronized OlcValSortValue parse( String s ) throws ParseException
    {
        if ( s == null )
        {
            return null;
        }

        // Trimming the value
        s = Strings.trim( s );

        // Getting the chars of the string
        char[] chars = s.toCharArray();

        // Creating the position
        Position pos = new Position();
        pos.start = 0;
        pos.end = 0;
        pos.length = chars.length;

        return parseInternal( chars, pos );
    }


    /**
     * Parses the given string.
     *
     * @param chars the characters
     * @param pos the position
     * @return the associated OlcValSortValue object
     * @throws ParseException
     */
    private static OlcValSortValue parseInternal( char[] chars, Position pos ) throws ParseException
    {
        OlcValSortValue olcValSortValue = new OlcValSortValue();

        // Empty (trimmed) string?
        if ( chars.length == 0 )
        {
            return null;
        }

        do
        {
            // Attribute
            String attribute = getQuotedOrNotQuotedOptionValue( chars, pos );

            if ( ( attribute != null ) && ( !attribute.isEmpty() ) )
            {
                olcValSortValue.setAttribute( attribute );
            }
            else
            {
                throw new ParseException( "Could not find the 'Attribute' value", pos.start );
            }

            // Base DN
            String baseDn = getQuotedOrNotQuotedOptionValue( chars, pos );

            if ( ( baseDn != null ) && ( !baseDn.isEmpty() ) )
            {
                try
                {
                    olcValSortValue.setBaseDn( new Dn( baseDn ) );
                }
                catch ( LdapInvalidDnException e )
                {
                    throw new ParseException( "Could not convert '" + baseDn + "' to a valid DN.", pos.start );
                }
            }
            else
            {
                throw new ParseException( "Could not find the 'Base DN value", pos.start );
            }

            // Getting the next item
            // It can either "weighted" or a sort method
            String weightedOrSortMethod = getQuotedOrNotQuotedOptionValue( chars, pos );

            if ( ( weightedOrSortMethod != null ) && ( !weightedOrSortMethod.isEmpty() ) )
            {
                // Weighted
                if ( isWeighted( weightedOrSortMethod ) )
                {
                    olcValSortValue.setWeighted( true );
                }
                // Sort Method
                else if ( isSortMethod( weightedOrSortMethod ) )
                {
                    olcValSortValue.setSortMethod( OlcValSortMethodEnum.fromString( weightedOrSortMethod ) );
                }
                else
                {
                    throw new ParseException( "Could not identify keyword '" + weightedOrSortMethod
                        + "' as a valid sort method.", pos.start );
                }
            }

            // Getting the next item
            // It should not exist if the previous item was "weighted" and
            // must a sort method it the previous item was "weighted"
            String sortMethod = getQuotedOrNotQuotedOptionValue( chars, pos );

            if ( ( sortMethod != null ) && ( !sortMethod.isEmpty() ) )
            {
                if ( olcValSortValue.isWeighted() )
                {
                    if ( isSortMethod( sortMethod ) )
                    {
                        olcValSortValue.setSortMethod( OlcValSortMethodEnum.fromString( sortMethod ) );
                    }
                    else
                    {
                        throw new ParseException( "Could not identify keyword '" + sortMethod
                            + "' as a valid sort method.", pos.start );
                    }
                }
                else
                {
                    throw new ParseException( "Keyword '" + sortMethod + "' is not allowed after sort method.",
                        pos.start );
                }
            }
        }
        while ( ( pos.start != pos.length ) && ( ( Strings.charAt( chars, pos.start ) ) != '\0' ) );

        return olcValSortValue;
    }


    /**
     * Indicates if the given string is "weighted".
     *
     * @param s the string to test
     * @return <code>true</code> if the given string is "weighted",
     *         <code>false</code> if not.
     */
    private static boolean isWeighted( String s )
    {
        return WEIGHTED_STRING.equalsIgnoreCase( s );
    }


    /**
     * Indicates if the given string is one of the sort methods.
     *
     * @param s the string to test
     * @return <code>true</code> if the given string is one of the sort methods,
     *         <code>false</code> if not.
     */
    private static boolean isSortMethod( String s )
    {
        return ( OlcValSortMethodEnum.fromString( s ) != null );
    }


    private static String getQuotedOrNotQuotedOptionValue( char[] chars, Position pos ) throws ParseException
    {
        if ( pos.start != pos.length )
        {
            char quoteChar = '\0';
            boolean isInQuotes = false;
            char c = Strings.charAt( chars, pos.start );
            char[] v = new char[chars.length - pos.start];
            int current = 0;

            do
            {
                if ( ( current == 0 ) && !isInQuotes )
                {
                    // Whitespace
                    if ( Character.isWhitespace( c ) )
                    {
                        // We ignore all whitespaces until we find the start of the value
                        pos.start++;
                        continue;
                    }
                    // Double quotes (") or single quotes (')
                    else if ( ( c == '"' ) || ( c == '\'' ) )
                    {
                        isInQuotes = true;
                        quoteChar = c;
                        pos.start++;
                        continue;
                    }
                    // Any other char is part of a value
                    else
                    {
                        v[current++] = c;
                        pos.start++;
                    }
                }
                else
                {
                    if ( isInQuotes )
                    {
                        // Double quotes (") or single quotes (')
                        if ( quoteChar == c )
                        {
                            isInQuotes = false;
                            pos.start++;
                            continue;
                        }
                        // Checking for escaped quotes
                        else if ( c == '\\' )
                        {
                            // Double quotes (")
                            if ( ( quoteChar == '"' ) && ( Strings.areEquals( chars, pos.start, "\\\"" ) >= 0 ) )
                            {
                                v[current++] = '"';
                                pos.start += 2;
                                continue;
                            }
                            // Single quotes (')
                            else if ( ( quoteChar == '\'' ) && ( Strings.areEquals( chars, pos.start, "\\'" ) >= 0 ) )
                            {
                                v[current++] = '\'';
                                pos.start += 2;
                                continue;
                            }
                        }
                        // Any other char is part of a value
                        else
                        {
                            v[current++] = c;
                            pos.start++;
                        }
                    }
                    else
                    {
                        // Whitespace
                        if ( Character.isWhitespace( c ) )
                        {
                            // Once we have found the start of the value, the first whitespace is the exit
                            break;
                        }
                        // Any other char is part of a value
                        else
                        {
                            v[current++] = c;
                            pos.start++;
                        }
                    }
                }
            }
            while ( ( c = Strings.charAt( chars, pos.start ) ) != '\0' );

            // Checking the resulting value
            if ( current == 0 )
            {
                throw new ParseException( "Couldn't find a value.", pos.start );
            }

            char[] value = new char[current];
            System.arraycopy( v, 0, value, 0, current );

            // Getting the value as a String
            return new String( value );
        }

        return null;
    }
}
