/*
 *  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
 * 
 *    https://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.api.ldap.model.schema;


import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.ldap.model.constants.MetaSchemaConstants;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Modification;
import org.apache.directory.api.ldap.model.entry.Value;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.util.Strings;


/**
 * Various utility methods for schema functions and objects.
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public final class SchemaUtils
{
    /**
     * Private constructor.
     */
    private SchemaUtils()
    {
    }


    /**
     * Gets the target entry as it would look after a modification operation
     * were performed on it.
     * 
     * @param mods the modifications performed on the entry
     * @param entry the source entry that is modified
     * @return the resultant entry after the modifications have taken place
     * @throws LdapException if there are problems accessing attributes
     */
    public static Entry getTargetEntry( List<? extends Modification> mods, Entry entry )
        throws LdapException
    {
        Entry targetEntry = entry.clone();

        for ( Modification mod : mods )
        {
            String id = mod.getAttribute().getId();

            switch ( mod.getOperation() )
            {
                case REPLACE_ATTRIBUTE:
                    targetEntry.put( mod.getAttribute() );
                    break;

                case ADD_ATTRIBUTE:
                    Attribute combined = mod.getAttribute().clone();
                    Attribute toBeAdded = mod.getAttribute();
                    Attribute existing = entry.get( id );

                    if ( existing != null )
                    {
                        for ( Value value : existing )
                        {
                            combined.add( value );
                        }
                    }

                    for ( Value value : toBeAdded )
                    {
                        combined.add( value );
                    }

                    targetEntry.put( combined );
                    break;

                case REMOVE_ATTRIBUTE:
                    Attribute toBeRemoved = mod.getAttribute();

                    if ( toBeRemoved.size() == 0 )
                    {
                        targetEntry.removeAttributes( id );
                    }
                    else
                    {
                        existing = targetEntry.get( id );

                        if ( existing != null )
                        {
                            for ( Value value : toBeRemoved )
                            {
                                existing.remove( value );
                            }
                        }
                    }

                    break;
                    
                case INCREMENT_ATTRIBUTE:
                    // The incremented attribute might not exist
                    AttributeType attributeType = mod.getAttribute().getAttributeType();
                    String incrementStr = mod.getAttribute().getString();
                    int increment = 1;
                    
                    if ( !Strings.isEmpty( incrementStr ) )
                    {
                        try
                        { 
                            increment = Integer.parseInt( incrementStr );
                        }
                        catch ( NumberFormatException nfe )
                        {
                            throw new IllegalArgumentException( I18n.err( I18n.ERR_13866_MOD_INCREMENT_INVALID_VALUE,  
                                attributeType.getName(), incrementStr ) );
                        }
                    }
                    Attribute modified = targetEntry.get( attributeType );

                    if ( !targetEntry.containsAttribute( attributeType ) )
                    {
                        throw new IllegalArgumentException( I18n.err( I18n.ERR_13867_MOD_INCREMENT_NO_ATTRIBUTE,  
                            attributeType.getName() ) );
                    }

                    if ( !SchemaConstants.INTEGER_SYNTAX.equals( modified.getAttributeType().getSyntax().getOid() ) )
                    {
                        throw new IllegalArgumentException( I18n.err( I18n.ERR_13868_MOD_INCREMENT_NO_INT_ATTRIBUTE,  
                            attributeType.getName() ) );
                    }
                    else
                    {
                        Value[] newValues = new Value[ modified.size() ];
                        int i = 0;
                        
                        for ( Value value : modified )
                        {
                            int intValue = Integer.parseInt( value.getNormalized() );
                            
                            if ( intValue == Integer.MAX_VALUE )
                            {
                                throw new IllegalArgumentException( I18n.err( I18n.ERR_13869_MOD_INCREMENT_OVERFLOW,  
                                    attributeType.getName(), intValue ) );
                            }
                            
                            newValues[i++] = new Value( Integer.toString( intValue + increment ) );
                            modified.remove( value );
                        }
                        
                        modified.add( newValues );
                    }
                    
                    break;

                default:
                    throw new IllegalStateException( I18n.err( I18n.ERR_13775_UNDEFINED_MODIFICATION_TYPE, mod.getOperation() ) );
            }
        }

        return targetEntry;
    }


    // ------------------------------------------------------------------------
    // qdescrs rendering operations
    // ------------------------------------------------------------------------

    /**
     * Renders qdescrs into an existing buffer.
     * 
     * @param buf the string buffer to render the quoted description strs into
     * @param qdescrs the quoted description strings to render
     * @return the same string buffer that was given for call chaining
     */
    public static StringBuilder render( StringBuilder buf, List<String> qdescrs )
    {
        if ( ( qdescrs == null ) || qdescrs.isEmpty() )
        {
            return buf;
        }
        else if ( qdescrs.size() == 1 )
        {
            buf.append( "'" ).append( qdescrs.get( 0 ) ).append( "'" );
        }
        else
        {
            buf.append( "( " );

            for ( String qdescr : qdescrs )
            {
                buf.append( "'" ).append( qdescr ).append( "' " );
            }

            buf.append( ")" );
        }

        return buf;
    }


    /**
     * Renders qdescrs into a new buffer.<br>
     * <pre>
     * descrs ::= qdescr | '(' WSP qdescrlist WSP ')'
     * qdescrlist ::= [ qdescr ( SP qdescr )* ]
     * qdescr     ::= SQUOTE descr SQUOTE
     * </pre>
     * 
     * @param buf the string buffer to render the quoted description strings into
     * @param qdescrs the quoted description strings to render
     * @return the string buffer the qdescrs are rendered into
     */
    /* No qualifier */static StringBuilder renderQDescrs( StringBuilder buf, List<String> qdescrs )
    {
        if ( ( qdescrs == null ) || qdescrs.isEmpty() )
        {
            return buf;
        }

        if ( qdescrs.size() == 1 )
        {
            buf.append( '\'' ).append( qdescrs.get( 0 ) ).append( '\'' );
        }
        else
        {
            buf.append( "( " );

            for ( String qdescr : qdescrs )
            {
                buf.append( '\'' ).append( qdescr ).append( "' " );
            }

            buf.append( ")" );
        }

        return buf;
    }


    /**
     * Renders QDString into a new buffer.<br>
     * 
     * @param buf the string buffer to render the quoted description string into
     * @param qdString the quoted description strings to render
     * @return the string buffer the qdescrs are rendered into
     */
    private static StringBuilder renderQDString( StringBuilder buf, String qdString )
    {
        buf.append( '\'' );

        for ( char c : qdString.toCharArray() )
        {
            switch ( c )
            {
                case 0x27:
                    buf.append( "\\27" );
                    break;

                case 0x5C:
                    buf.append( "\\5C" );
                    break;

                default:
                    buf.append( c );
                    break;
            }
        }

        buf.append( '\'' );

        return buf;
    }


    // ------------------------------------------------------------------------
    // objectClass list rendering operations
    // ------------------------------------------------------------------------

    /**
     * Renders a list of object classes for things like a list of superior
     * objectClasses using the ( oid $ oid ) format.
     * 
     * @param ocs
     *            the objectClasses to list
     * @return a buffer which contains the rendered list
     */
    public static StringBuilder render( ObjectClass[] ocs )
    {
        StringBuilder buf = new StringBuilder();

        return render( buf, ocs );
    }


    /**
     * Renders a list of object classes for things like a list of superior
     * objectClasses using the ( oid $ oid ) format into an existing buffer.
     * 
     * @param buf
     *            the string buffer to render the list of objectClasses into
     * @param ocs
     *            the objectClasses to list
     * @return a buffer which contains the rendered list
     */
    public static StringBuilder render( StringBuilder buf, ObjectClass[] ocs )
    {
        if ( ocs == null || ocs.length == 0 )
        {
            return buf;
        }
        else if ( ocs.length == 1 )
        {
            buf.append( ocs[0].getName() );
        }
        else
        {
            buf.append( "( " );

            for ( int ii = 0; ii < ocs.length; ii++ )
            {
                if ( ii + 1 < ocs.length )
                {
                    buf.append( ocs[ii].getName() ).append( " $ " );
                }
                else
                {
                    buf.append( ocs[ii].getName() );
                }
            }

            buf.append( " )" );
        }

        return buf;
    }


    // ------------------------------------------------------------------------
    // attributeType list rendering operations
    // ------------------------------------------------------------------------

    /**
     * Renders a list of attributeTypes for things like the must or may list of
     * objectClasses using the ( oid $ oid ) format.
     * 
     * @param ats
     *            the attributeTypes to list
     * @return a buffer which contains the rendered list
     */
    public static StringBuilder render( AttributeType[] ats )
    {
        StringBuilder buf = new StringBuilder();
        return render( buf, ats );
    }


    /**
     * Renders a list of attributeTypes for things like the must or may list of
     * objectClasses using the ( oid $ oid ) format into an existing buffer.
     * 
     * @param buf
     *            the string buffer to render the list of attributeTypes into
     * @param ats
     *            the attributeTypes to list
     * @return a buffer which contains the rendered list
     */
    public static StringBuilder render( StringBuilder buf, AttributeType[] ats )
    {
        if ( ats == null || ats.length == 0 )
        {
            return buf;
        }
        else if ( ats.length == 1 )
        {
            buf.append( ats[0].getName() );
        }
        else
        {
            buf.append( "( " );
            for ( int ii = 0; ii < ats.length; ii++ )
            {
                if ( ii + 1 < ats.length )
                {
                    buf.append( ats[ii].getName() ).append( " $ " );
                }
                else
                {
                    buf.append( ats[ii].getName() );
                }
            }
            buf.append( " )" );
        }

        return buf;
    }


    // ------------------------------------------------------------------------
    // schema object rendering operations
    // ------------------------------------------------------------------------

    /**
     * Renders the schema extensions into a new StringBuffer.
     *
     * @param extensions the schema extensions map with key and values
     * @return a StringBuffer with the extensions component of a syntax description
     */
    public static StringBuilder render( Map<String, List<String>> extensions )
    {
        StringBuilder buf = new StringBuilder();

        if ( extensions.isEmpty() )
        {
            return buf;
        }

        for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
        {
            buf.append( " " ).append( entry.getKey() ).append( " " );

            List<String> values = entry.getValue();

            // For extensions without values like X-IS-HUMAN-READIBLE
            if ( values == null || values.isEmpty() )
            {
                continue;
            }

            // For extensions with a single value we can use one qdstring like 'value'
            if ( values.size() == 1 )
            {
                buf.append( "'" ).append( values.get( 0 ) ).append( "' " );
                continue;
            }

            // For extensions with several values we have to surround whitespace
            // separated list of qdstrings like ( 'value0' 'value1' 'value2' )
            buf.append( "( " );
            for ( String value : values )
            {
                buf.append( "'" ).append( value ).append( "' " );
            }
            buf.append( ")" );
        }

        if ( buf.charAt( buf.length() - 1 ) != ' ' )
        {
            buf.append( " " );
        }

        return buf;
    }


    /**
     * Returns a String description of a schema. The resulting String format is :
     * <br>
     * (OID [DESC '&lt;description&gt;'] FQCN &lt;fcqn&gt; [BYTECODE &lt;bytecode&gt;] X-SCHEMA '&lt;schema&gt;')
     * <br>
     * @param description The description to transform to a String
     * @return The rendered schema object
     */
    public static String render( LoadableSchemaObject description )
    {
        StringBuilder buf = new StringBuilder();
        buf.append( "( " ).append( description.getOid() );

        if ( description.getDescription() != null )
        {
            buf.append( " DESC " );
            renderQDString( buf, description.getDescription() );
        }

        buf.append( " FQCN " ).append( description.getFqcn() );

        if ( !Strings.isEmpty( description.getBytecode() ) )
        {
            buf.append( " BYTECODE " ).append( description.getBytecode() );
        }

        buf.append( " X-SCHEMA '" );
        buf.append( getSchemaName( description ) );
        buf.append( "' )" );

        return buf.toString();
    }


    private static String getSchemaName( SchemaObject desc )
    {
        List<String> values = desc.getExtension( MetaSchemaConstants.X_SCHEMA_AT );

        if ( values == null || values.isEmpty() )
        {
            return MetaSchemaConstants.SCHEMA_OTHER;
        }

        return values.get( 0 );
    }


    /**
     * Remove the options from the attributeType, and returns the ID.
     * <br>
     * RFC 4512 :
     * <pre>
     * attributedescription = attributetype options
     * attributetype = oid
     * options = *( SEMI option )
     * option = 1*keychar
     * </pre>
     * 
     * @param attributeId The AttributeType to parse
     * @return The AttributeType without its options
     */
    public static String stripOptions( String attributeId )
    {
        int optionsPos = attributeId.indexOf( ';' );

        if ( optionsPos != -1 )
        {
            return attributeId.substring( 0, optionsPos );
        }
        else
        {
            return attributeId;
        }
    }


    /**
     * Get the options from the attributeType.
     * <br>
     * For instance, given :
     * jpegphoto;binary;lang=jp
     * <br>
     * your get back a set containing { "binary", "lang=jp" }
     * 
     * @param attributeId The AttributeType to parse
     * @return a Set of options found for this AttributeType, or null
     */
    public static Set<String> getOptions( String attributeId )
    {
        int optionsPos = attributeId.indexOf( ';' );

        if ( optionsPos != -1 )
        {
            Set<String> options = new HashSet<>();

            String[] res = attributeId.substring( optionsPos + 1 ).split( ";" );

            for ( String option : res )
            {
                if ( !Strings.isEmpty( option ) )
                {
                    options.add( option );
                }
            }

            return options;
        }
        else
        {
            return null;
        }
    }


    /**
     * Transform an UUID in a byte array
     * @param uuid The UUID to transform
     * @return The byte[] representing the UUID
     */
    public static byte[] uuidToBytes( UUID uuid )
    {
        Long low = uuid.getLeastSignificantBits();
        Long high = uuid.getMostSignificantBits();
        byte[] bytes = new byte[16];

        bytes[0] = ( byte ) ( ( high & 0xff00000000000000L ) >> 56 );
        bytes[1] = ( byte ) ( ( high & 0x00ff000000000000L ) >> 48 );
        bytes[2] = ( byte ) ( ( high & 0x0000ff0000000000L ) >> 40 );
        bytes[3] = ( byte ) ( ( high & 0x000000ff00000000L ) >> 32 );
        bytes[4] = ( byte ) ( ( high & 0x00000000ff000000L ) >> 24 );
        bytes[5] = ( byte ) ( ( high & 0x0000000000ff0000L ) >> 16 );
        bytes[6] = ( byte ) ( ( high & 0x000000000000ff00L ) >> 8 );
        bytes[7] = ( byte ) ( high & 0x00000000000000ffL );
        bytes[8] = ( byte ) ( ( low & 0xff00000000000000L ) >> 56 );
        bytes[9] = ( byte ) ( ( low & 0x00ff000000000000L ) >> 48 );
        bytes[10] = ( byte ) ( ( low & 0x0000ff0000000000L ) >> 40 );
        bytes[11] = ( byte ) ( ( low & 0x000000ff00000000L ) >> 32 );
        bytes[12] = ( byte ) ( ( low & 0x00000000ff000000L ) >> 24 );
        bytes[13] = ( byte ) ( ( low & 0x0000000000ff0000L ) >> 16 );
        bytes[14] = ( byte ) ( ( low & 0x000000000000ff00L ) >> 8 );
        bytes[15] = ( byte ) ( low & 0x00000000000000ffL );

        return bytes;
    }


    /**
     * Tells if an AttributeType name is valid or not. An Attribute name is valid if 
     * it's a descr / numericoid, as described in rfc4512 :
     * <pre>
     * name = descr / numericOid
     * descr = keystring
     * keystring = leadkeychar *keychar
     * leadkeychar = ALPHA
     * keychar = ALPHA / DIGIT / HYPHEN / USCORE
     * numericoid = number 1*( DOT number )
     * number  = DIGIT / ( LDIGIT 1*DIGIT )
     * ALPHA   = %x41-5A / %x61-7A   ; "A"-"Z" / "a"-"z"
     * DIGIT   = %x30 / LDIGIT       ; "0"-"9"
     * HYPHEN  = %x2D ; hyphen ("-")
     * LDIGIT  = %x31-39             ; "1"-"9"
     * DOT     = %x2E ; period (".")
     * USCORE  = %x5F ; underscore ("_")
     * </pre>
     * 
     * Note that we have extended this grammar to accept the '_' char, which is widely used in teh LDAP world.
     *
     * @param attributeName The AttributeType name to check
     * @return true if it's valid
     */
    public static boolean isAttributeNameValid( String attributeName )
    {
        if ( Strings.isEmpty( attributeName ) )
        {
            return false;
        }
        
        // Check the first char which must be ALPHA or DIGIT
        boolean descr;
        boolean zero = false;
        boolean dot = false;
        
        char c = attributeName.charAt( 0 );
        
        if ( ( ( c >= 'a' ) && ( c <= 'z' ) ) || ( ( c >= 'A' ) && ( c <= 'Z' ) ) )
        {
            descr = true;
        }
        else if ( ( c >= '0' ) && ( c <= '9' ) )
        {
            descr = false;
            
            zero = c == '0'; 
        }
        else
        {
            return false;
        }
        
        for ( int i = 1; i < attributeName.length(); i++ )
        {
            c = attributeName.charAt( i ); 
            
            if ( descr )
            {
                // This is a descr, iterate on KeyChars (ALPHA / DIGIT / HYPHEN / USCORE)
                if ( ( ( c < 'a' ) || ( c > 'z' ) )
                    && ( ( c < 'A' ) || ( c > 'Z' ) )
                    && ( ( c < '0' ) || ( c > '9' ) )
                    && ( c != '-' )
                    && ( c != '_' ) )
                {
                    return false;
                }
            }
            else
            {
                // This is a numericOid, check it
                if ( c == '.' )
                {
                    // Not allowed if we already have had a dot
                    if ( dot )
                    {
                        return false;
                    }
                    
                    dot = true;
                    zero = false;
                }
                else if ( ( c >= '0' ) && ( c <= '9' ) )
                {
                    dot = false;
                    
                    if ( zero )
                    {
                        // We can't have a leading '0' followed by another number
                        return false;
                    }
                    else if ( c == '0' )
                    {
                        zero = true;
                    }
                }
                else
                {
                    // Not valid
                    return false;
                }
            }
        }
        
        return true;
    }
}
