/*
 *  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.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.util.Strings;


/**
 * Most schema objects have some common attributes. This class
 * contains the minimum set of properties exposed by a SchemaObject.<br>
 * We have 11 types of SchemaObjects :
 * <ul>
 *   <li> AttributeType</li>
 *   <li> DitCOntentRule</li>
 *   <li> DitStructureRule</li>
 *   <li> LdapComparator (specific to ADS)</li>
 *   <li> LdapSyntaxe</li>
 *   <li> MatchingRule</li>
 *   <li> MatchingRuleUse</li>
 *   <li> NameForm</li>
 *   <li> Normalizer (specific to ADS)</li>
 *   <li> ObjectClass</li>
 *   <li> SyntaxChecker (specific to ADS)</li>
 * </ul>
 * <br>
 * <br>
 * This class provides accessors and setters for the following attributes,
 * which are common to all those SchemaObjects :
 * <ul>
 *  <li>oid : The numeric OID</li>
 *   <li>description : The SchemaObject description</li>
 *   <li>obsolete : Tells if the schema object is obsolete</li>
 *   <li>extensions : The extensions, a key/Values map</li>
 *   <li>schemaObjectType : The SchemaObject type (see upper)</li>
 *   <li>schema : The schema the SchemaObject is associated with (it's an extension).
 *     Can be null</li>
 *   <li>isEnabled : The SchemaObject status (it's related to the schema status)</li>
 *   <li>isReadOnly : Tells if the SchemaObject can be modified or not</li>
 * </ul>
 * <br><br>
 * Some of those attributes are not used by some Schema elements, even if they should
 * have been used. Here is the list :
 * <ul>
 *   <li><b>name</b> : LdapSyntax, Comparator, Normalizer, SyntaxChecker</li>
 *   <li><b>numericOid</b> : DitStructureRule</li>
 *   <li><b>obsolete</b> : LdapSyntax, Comparator, Normalizer, SyntaxChecker</li>
 * </ul>
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public abstract class AbstractSchemaObject implements SchemaObject, Serializable
{
    /** The serial version UID */
    private static final long serialVersionUID = 2L;

    /** The SchemaObject numeric OID */
    protected String oid;

    /** The optional names for this SchemaObject */
    protected transient List<String> names;

    /** Whether or not this SchemaObject is enabled */
    protected boolean isEnabled = true;

    /** Whether or not this SchemaObject is obsolete */
    protected boolean isObsolete = false;

    /** A short description of this SchemaObject */
    protected String description;

    /** The SchemaObject specification */
    protected String specification;

    /** The name of the schema this object is associated with */
    protected String schemaName;

    /** The SchemaObjectType */
    protected SchemaObjectType objectType;

    /** A map containing the list of supported extensions */
    protected transient Map<String, List<String>> extensions;

    /** A locked to avoid modifications when set to true */
    protected volatile boolean locked;

    /** The hashcode for this schemaObject */
    protected volatile int h = 0;


    /**
     * A constructor for a SchemaObject instance. It must be
     * invoked by the inherited class.
     *
     * @param objectType The SchemaObjectType to create
     * @param oid the SchemaObject numeric OID
     */
    protected AbstractSchemaObject( SchemaObjectType objectType, String oid )
    {
        this.objectType = objectType;
        this.oid = oid;
        extensions = new HashMap<>();
        names = new ArrayList<>();
        computeHashCode();
    }


    /**
     * Constructor used when a generic reusable SchemaObject is assigned an
     * OID after being instantiated.
     * 
     * @param objectType The SchemaObjectType to create
     */
    protected AbstractSchemaObject( SchemaObjectType objectType )
    {
        this.objectType = objectType;
        extensions = new HashMap<>();
        names = new ArrayList<>();
    }


    /**
     * Gets usually what is the numeric object identifier assigned to this
     * SchemaObject. All schema objects except for MatchingRuleUses have an OID
     * assigned specifically to then. A MatchingRuleUse's OID really is the OID
     * of it's MatchingRule and not specific to the MatchingRuleUse. This
     * effects how MatchingRuleUse objects are maintained by the system.
     * 
     * @return an OID for this SchemaObject or its MatchingRule if this
     *         SchemaObject is a MatchingRuleUse object
     */
    @Override
    public String getOid()
    {
        return oid;
    }


    /**
     * A special method used when renaming an SchemaObject: we may have to
     * change it's OID
     * @param oid The new OID
     */
    @Override
    public void setOid( String oid )
    {
        if ( locked )
        {
            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
        }

        this.oid = oid;
        
        computeHashCode();
    }


    /**
     * Gets short names for this SchemaObject if any exists for it, otherwise,
     * returns an empty list.
     * 
     * @return the names for this SchemaObject
     */
    @Override
    public List<String> getNames()
    {
        if ( names != null )
        {
            return Collections.unmodifiableList( names );
        }
        else
        {
            return Collections.emptyList();
        }
    }


    /**
     * Gets the first name in the set of short names for this SchemaObject if
     * any exists for it.
     * 
     * @return the first of the names for this SchemaObject or the oid
     * if one does not exist
     */
    @Override
    public String getName()
    {
        if ( ( names != null ) && !names.isEmpty() )
        {
            return names.get( 0 );
        }
        else
        {
            return oid;
        }
    }


    /**
     * Add a new name to the list of names for this SchemaObject. The name
     * is lower cased and trimmed.
     * 
     * @param namesToAdd The names to add
     */
    @Override
    public void addName( String... namesToAdd )
    {
        if ( locked )
        {
            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
        }

        // We must avoid duplicated names, as names are case insensitive
        Set<String> lowerNames = new HashSet<>();

        // Fills a set with all the existing names
        for ( String name : this.names )
        {
            lowerNames.add( Strings.toLowerCaseAscii( name ) );
        }

        for ( String name : namesToAdd )
        {
            if ( name != null )
            {
                String lowerName = Strings.toLowerCaseAscii( name );
                // Check that the lower cased names is not already present
                if ( !lowerNames.contains( lowerName ) )
                {
                    this.names.add( name );
                    lowerNames.add( lowerName );
                }
            }
        }
        
        computeHashCode();
    }


    /**
     * Sets the list of names for this SchemaObject. The names are
     * lowercased and trimmed.
     * 
     * @param names The list of names. Can be empty
     */
    @Override
    public void setNames( List<String> names )
    {
        if ( locked )
        {
            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
        }

        if ( names == null )
        {
            return;
        }

        this.names = new ArrayList<>( names.size() );

        for ( String name : names )
        {
            if ( name != null )
            {
                this.names.add( name );
            }
        }
        
        computeHashCode();
    }


    /**
     * Sets the list of names for this SchemaObject. The names are
     * lowercased and trimmed.
     * 
     * @param names The list of names.
     */
    public void setNames( String... names )
    {
        if ( locked )
        {
            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
        }

        if ( names == null )
        {
            return;
        }

        this.names.clear();

        for ( String name : names )
        {
            if ( name != null )
            {
                this.names.add( name );
            }
        }
        
        computeHashCode();
    }


    /**
     * Gets a short description about this SchemaObject.
     * 
     * @return a short description about this SchemaObject
     */
    @Override
    public String getDescription()
    {
        return description;
    }


    /**
     * Sets the SchemaObject's description
     * 
     * @param description The SchemaObject's description
     */
    @Override
    public void setDescription( String description )
    {
        if ( locked )
        {
            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
        }

        this.description = description;
        
        computeHashCode();
    }


    /**
     * Gets the SchemaObject specification.
     * 
     * @return the SchemaObject specification
     */
    @Override
    public String getSpecification()
    {
        return specification;
    }


    /**
     * Sets the SchemaObject's specification
     * 
     * @param specification The SchemaObject's specification
     */
    @Override
    public void setSpecification( String specification )
    {
        if ( locked )
        {
            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
        }

        this.specification = specification;
        
        computeHashCode();
    }


    /**
     * Tells if this SchemaObject is enabled.
     * 
     * @return true if the SchemaObject is enabled, or if it depends on
     * an enabled schema
     */
    @Override
    public boolean isEnabled()
    {
        return isEnabled;
    }


    /**
     * Tells if this SchemaObject is disabled.
     * 
     * @return true if the SchemaObject is disabled
     */
    @Override
    public boolean isDisabled()
    {
        return !isEnabled;
    }


    /**
     * Sets the SchemaObject state, either enabled or disabled.
     * 
     * @param enabled The current SchemaObject state
     */
    @Override
    public void setEnabled( boolean enabled )
    {
        if ( locked )
        {
            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
        }

        isEnabled = enabled;
        
        computeHashCode();
    }


    /**
     * Gets whether or not this SchemaObject has been inactivated. All
     * SchemaObjects except Syntaxes allow for this parameter within their
     * definition. For Syntaxes this property should always return false in
     * which case it is never included in the description.
     * 
     * @return true if inactive, false if active
     */
    @Override
    public boolean isObsolete()
    {
        return isObsolete;
    }


    /**
     * Sets the Obsolete flag.
     * 
     * @param obsolete The Obsolete flag state
     */
    @Override
    public void setObsolete( boolean obsolete )
    {
        if ( locked )
        {
            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
        }

        this.isObsolete = obsolete;
        
        computeHashCode();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Map<String, List<String>> getExtensions()
    {
        return extensions;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasExtension( String extension )
    {
        return extensions.containsKey( Strings.toUpperCaseAscii( extension ) );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public List<String> getExtension( String extension )
    {
        String name = Strings.toUpperCaseAscii( extension );
        
        if ( hasExtension( name ) )
        {
            for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
            {
                String key = entry.getKey();
                
                if ( name.equalsIgnoreCase( key ) )
                {
                    return entry.getValue();
                }
            }
        }

        return null;
    }


    /**
     * Add an extension with its values
     * @param key The extension key
     * @param values The associated values
     */
    @Override
    public void addExtension( String key, String... values )
    {
        if ( locked )
        {
            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
        }

        List<String> valueList = new ArrayList<>();

        for ( String value : values )
        {
            valueList.add( value );
        }

        extensions.put( Strings.toUpperCaseAscii( key ), valueList );
        
        computeHashCode();
    }


    /**
     * Add an extension with its values
     * @param key The extension key
     * @param values The associated values
     */
    @Override
    public void addExtension( String key, List<String> values )
    {
        if ( locked )
        {
            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
        }

        extensions.put( Strings.toUpperCaseAscii( key ), values );
        
        computeHashCode();
    }


    /**
     * Add an extensions with their values. (Actually do a copy)
     * 
     * @param extensions The extensions map
     */
    @Override
    public void setExtensions( Map<String, List<String>> extensions )
    {
        if ( locked )
        {
            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
        }

        if ( extensions != null )
        {
            this.extensions = new HashMap<>();

            for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
            {
                List<String> values = new ArrayList<>();

                for ( String value : entry.getValue() )
                {
                    values.add( value );
                }

                this.extensions.put( Strings.toUpperCaseAscii( entry.getKey() ), values );
            }

            computeHashCode();
        }
    }


    /**
     * The SchemaObject type :
     * <ul>
     *   <li> AttributeType
     *   <li> DitCOntentRule
     *   <li> DitStructureRule
     *   <li> LdapComparator (specific to ADS)
     *   <li> LdapSyntaxe
     *   <li> MatchingRule
     *   <li> MatchingRuleUse
     *   <li> NameForm
     *   <li> Normalizer (specific to ADS)
     *   <li> ObjectClass
     *   <li> SyntaxChecker (specific to ADS)
     * </ul>
     * 
     * @return the SchemaObject type
     */
    @Override
    public SchemaObjectType getObjectType()
    {
        return objectType;
    }


    /**
     * Gets the name of the schema this SchemaObject is associated with.
     *
     * @return the name of the schema associated with this schemaObject
     */
    @Override
    public String getSchemaName()
    {
        return schemaName;
    }


    /**
     * Sets the name of the schema this SchemaObject is associated with.
     * 
     * @param schemaName the new schema name
     */
    @Override
    public void setSchemaName( String schemaName )
    {
        if ( locked )
        {
            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
        }

        this.schemaName = schemaName;
        
        computeHashCode();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals( Object o1 )
    {
        if ( this == o1 )
        {
            return true;
        }

        if ( !( o1 instanceof AbstractSchemaObject ) )
        {
            return false;
        }

        AbstractSchemaObject that = ( AbstractSchemaObject ) o1;

        // Two schemaObject are equals if their oid is equal,
        // their ObjectType is equal, their names are equals
        // their schema name is the same, all their flags are equals,
        // the description is the same and their extensions are equals
        if ( !compareOid( oid, that.oid ) )
        {
            return false;
        }

        // Compare the names
        if ( names == null )
        {
            if ( that.names != null )
            {
                return false;
            }
        }
        else if ( that.names == null )
        {
            return false;
        }
        else
        {
            int nbNames = 0;

            for ( String name : names )
            {
                if ( !that.names.contains( name ) )
                {
                    return false;
                }

                nbNames++;
            }

            if ( nbNames != names.size() )
            {
                return false;
            }
        }

        if ( schemaName == null )
        {
            if ( that.schemaName != null )
            {
                return false;
            }
        }
        else
        {
            if ( !schemaName.equalsIgnoreCase( that.schemaName ) )
            {
                return false;
            }
        }

        if ( objectType != that.objectType )
        {
            return false;
        }

        if ( extensions != null )
        {
            if ( that.extensions == null )
            {
                return false;
            }
            else
            {
                for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
                {
                    String key = entry.getKey();
                    
                    if ( !that.extensions.containsKey( key ) )
                    {
                        return false;
                    }

                    List<String> thisValues = entry.getValue();
                    List<String> thatValues = that.extensions.get( key );

                    if ( thisValues != null )
                    {
                        if ( thatValues == null )
                        {
                            return false;
                        }
                        else
                        {
                            if ( thisValues.size() != thatValues.size() )
                            {
                                return false;
                            }

                            // TODO compare the values
                        }
                    }
                    else if ( thatValues != null )
                    {
                        return false;
                    }
                }
            }
        }
        else if ( that.extensions != null )
        {
            return false;
        }

        if ( this.isEnabled != that.isEnabled )
        {
            return false;
        }

        if ( this.isObsolete != that.isObsolete )
        {
            return false;
        }

        if ( this.description == null )
        {
            return that.description == null;
        }
        else
        {
            return this.description.equalsIgnoreCase( that.description );
        }
    }


    /**
     * Compare two oids, and return true if they are both null or equal.
     *
     * @param oid1 the first OID
     * @param oid2 the second OID
     * @return <code>true</code> if both OIDs are null or equal
     */
    protected boolean compareOid( String oid1, String oid2 )
    {
        if ( oid1 == null )
        {
            return oid2 == null;
        }
        else
        {
            return oid1.equals( oid2 );
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public SchemaObject copy( SchemaObject original )
    {
        // copy the description
        description = original.getDescription();

        // copy the flags
        isEnabled = original.isEnabled();
        isObsolete = original.isObsolete();

        // copy the names
        names = new ArrayList<>();

        for ( String name : original.getNames() )
        {
            names.add( name );
        }

        // copy the extensions
        extensions = new HashMap<>();

        for ( String key : original.getExtensions().keySet() )
        {
            List<String> extensionValues = original.getExtension( key );

            List<String> cloneExtension = new ArrayList<>();

            for ( String value : extensionValues )
            {
                cloneExtension.add( value );
            }

            extensions.put( key, cloneExtension );
        }

        // The SchemaName
        schemaName = original.getSchemaName();

        // The specification
        specification = original.getSpecification();

        return this;
    }


    /**
     * Clear the current SchemaObject : remove all the references to other objects,
     * and all the Maps.
     */
    @Override
    public void clear()
    {
        // Clear the extensions
        for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
        {
            List<String> extensionList = entry.getValue();

            extensionList.clear();
        }

        extensions.clear();

        // Clear the names
        names.clear();
        
        computeHashCode();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public final void lock()
    {
        locked = true;
    }


    /**
     * Unlock the Schema Object and make it modifiable again.
     */
    public void unlock()
    {
        locked = false;
    }
    
    
    /**
     * Compute the hashcode, and store it in the 'h' variable
     */
    protected void computeHashCode()
    {
        int hash = 37;

        // The OID
        if ( oid != null )
        {
            hash += hash * 17 + oid.hashCode();
        }

        // The SchemaObject type
        if ( objectType != null )
        {
            hash += hash * 17 + objectType.getValue();
        }

        // The Names, if any
        if ( ( names != null ) && !names.isEmpty() )
        {
            int tempHash = 0;
            
            for ( String name : names )
            {
                tempHash *= name.hashCode();
            }
            
            hash = hash * 17 + tempHash;
        }

        // The schemaName if any
        if ( schemaName != null )
        {
            hash += hash * 17 + schemaName.hashCode();
        }

        hash += hash * 17 + ( isEnabled ? 1 : 0 );

        // The description, if any
        if ( description != null )
        {
            hash += hash * 17 + description.hashCode();
        }

        // The extensions, if any
        // Because the extensions and their values are stored un-ordered
        // we have to be careful when computing the hashcode so that it does
        // not depend on the extensions/values order
        int tempHash = 0;
        
        for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
        {
            String key = entry.getKey();
            int tempHash2 = key.hashCode();

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

            if ( values != null )
            {
                int tempHash3 = 0;
                
                for ( String value : values )
                {
                    tempHash3 += value.hashCode();
                }

                tempHash += tempHash2 * tempHash3;
            }
        }
        
        hash = hash * 17 + tempHash;
        
        h = hash;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode()
    {
        return h;
    }
}
