/*
 *  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.schemaeditor.controller;


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

import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.LdapSyntax;
import org.apache.directory.api.ldap.model.schema.MatchingRule;
import org.apache.directory.api.ldap.model.schema.ObjectClass;
import org.apache.directory.api.ldap.model.schema.SchemaObject;
import org.apache.directory.api.util.Strings;
import org.apache.directory.studio.schemaeditor.model.Schema;


/**
 * This class represents the SchemaHandler.
 * <p>
 * It used to handle the whole Schema (including schemas, attribute types,
 * object classes, matching rules and syntaxes).
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class SchemaHandler
{
    //
    // The Lists
    //
    /** The schemas List */
    private List<Schema> schemasList;
    /** The attribute types List */
    private List<AttributeType> attributeTypesList;
    /** The matching rules List */
    private List<MatchingRule> matchingRulesList;
    /** The object classes List */
    private List<ObjectClass> objectClassesList;
    /** The syntaxes List */
    private List<LdapSyntax> syntaxesList;

    //
    // The MultiMap (for fast searching)
    //
    /** The schemas MultiMap */
    private MultiValuedMap<String, Schema> schemasMap;
    /** The attribute types MultiMap */
    private MultiValuedMap<String, AttributeType> attributeTypesMap;
    /** The matching rules MultiMap */
    private MultiValuedMap<String, MatchingRule> matchingRulesMap;
    /** The object classes MultiMap */
    private MultiValuedMap<String, ObjectClass> objectClassesMap;
    /** The syntaxes MultiMap */
    private MultiValuedMap<String, LdapSyntax> syntaxesMap;

    //
    // The Listeners Lists
    //
    private List<SchemaHandlerListener> schemaHandlerListeners;


    /**
     * Creates a new instance of SchemaHandler.
     */
    public SchemaHandler()
    {
        // Lists
        schemasList = new ArrayList<Schema>();
        attributeTypesList = new ArrayList<AttributeType>();
        matchingRulesList = new ArrayList<MatchingRule>();;
        objectClassesList = new ArrayList<ObjectClass>();
        syntaxesList = new ArrayList<LdapSyntax>();

        // Maps
        schemasMap = new ArrayListValuedHashMap<>();
        attributeTypesMap = new ArrayListValuedHashMap<>();
        matchingRulesMap = new ArrayListValuedHashMap<>();
        objectClassesMap = new ArrayListValuedHashMap<>();
        syntaxesMap = new ArrayListValuedHashMap<>();

        // Listeners
        schemaHandlerListeners = new ArrayList<SchemaHandlerListener>();
    }


    /**
     * Gets the List of all the attribute types.
     *
     * @return
     *      the List of all the attribute types
     */
    public List<AttributeType> getAttributeTypes()
    {
        return attributeTypesList;
    }


    /**
     * Gets the List of all the matching rules.
     *
     * @return
     *      the List of all the matching rules
     */
    public List<MatchingRule> getMatchingRules()
    {
        return matchingRulesList;
    }


    /**
     * Gets the List of all the object classes.
     *
     * @return
     *      the List of all the object classes
     */
    public List<ObjectClass> getObjectClasses()
    {
        return objectClassesList;
    }


    /**
     * Gets the List of all the schemas.
     *
     * @return
     *      the List of all the schemas
     */
    public List<Schema> getSchemas()
    {
        return schemasList;
    }


    /**
     * Gets the List of all the matching rules.
     *
     * @return
     *      the List of all the matching rules
     */
    public List<LdapSyntax> getSyntaxes()
    {
        return syntaxesList;
    }


    /**
     * Gets an attribute type identified by an OID, or an alias.
     *
     * @param id
     *      an OID or an alias
     * @return
     *      the corresponding attribute type, or null if no one is found
     */
    public AttributeType getAttributeType( String id )
    {
        List<?> list = getAttributeTypeList( Strings.toLowerCase( id ) );

        if ( ( list != null ) && ( list.size() >= 1 ) )
        {
            return ( AttributeType ) list.get( 0 );
        }
        else
        {
            return null;
        }
    }


    /**
     * Get the attribute type(s) List identified by an OID, or an alias.
     *
     * @param id
     *      an OID or an alias
     * @return
     *      the corresponding attribute type(s) List or null if no one is found
     */
    public List<?> getAttributeTypeList( String id )
    {
        return ( List<?> ) attributeTypesMap.get( Strings.toLowerCase( id ) );
    }


    /**
     * Gets a matching rule identified by an OID, or an alias.
     *
     * @param id
     *      an OID or an alias
     * @return
     *      the corresponding matching rule, or null if no one is found
     */
    public MatchingRule getMatchingRule( String id )
    {
        List<?> list = getMatchingRuleList( Strings.toLowerCase( id ) );

        if ( ( list != null ) && ( list.size() >= 1 ) )
        {
            return ( MatchingRule ) list.get( 0 );
        }
        else
        {
            return null;
        }
    }


    /**
     * Gets a matching rule(s) List identified by an OID, or an alias.
     *
     * @param id
     *      an OID or an alias
     * @return
     *      the corresponding matching rule(s) List, or null if no one is found
     */
    public List<?> getMatchingRuleList( String id )
    {
        return ( List<?> ) matchingRulesMap.get( Strings.toLowerCase( id ) );
    }


    /**
     * Gets an object class identified by an OID, or an alias.
     *
     * @param id
     *      an OID or an alias
     * @return
     *      the corresponding object class, or null if no one is found
     */
    public ObjectClass getObjectClass( String id )
    {
        List<?> list = getObjectClassList( Strings.toLowerCase( id ) );

        if ( ( list != null ) && ( list.size() >= 1 ) )
        {
            return ( ObjectClass ) list.get( 0 );
        }
        else
        {
            return null;
        }
    }


    /**
     * Gets an object class(es) List identified by an OID, or an alias.
     *
     * @param id
     *      an OID or an alias
     * @return
     *      the corresponding object class(es) List, or null if no one is found
     */
    public List<?> getObjectClassList( String id )
    {
        return ( List<?> ) objectClassesMap.get( Strings.toLowerCase( id ) );
    }


    /**
     * Gets a schema identified by a name.
     *
     * @param name
     *      a name
     * @return
     *      the corresponding schema, or null if no one is found
     */
    public Schema getSchema( String name )
    {
        List<?> list = getSchemaList( Strings.toLowerCase( name ) );

        if ( ( list != null ) && ( list.size() >= 1 ) )
        {
            return ( Schema ) list.get( 0 );
        }
        else
        {
            return null;
        }
    }


    /**
     * Gets a schema(s) List identified by a name.
     *
     * @param name
     *      a name
     * @return
     *      the corresponding schema(s) List, or null if no one is found
     */
    public List<?> getSchemaList( String name )
    {
        return ( List<?> ) schemasMap.get( Strings.toLowerCase( name ) );
    }


    /**
     * Gets a syntax identified by an OID, or an alias.
     *
     * @param id
     *      an OID or an alias
     * @return
     *      the corresponding syntax, or null if no one is found
     */
    public LdapSyntax getSyntax( String id )
    {
        List<?> list = getSyntaxList( Strings.toLowerCase( id ) );

        if ( ( list != null ) && ( list.size() >= 1 ) )
        {
            return ( LdapSyntax ) list.get( 0 );
        }
        else
        {
            return null;
        }
    }


    /**
     * Gets a syntax(es) List identified by an OID, or an alias.
     *
     * @param id
     *      an OID or an alias
     * @return
     *      the corresponding syntax(es) List, or null if no one is found
     */
    public List<?> getSyntaxList( String id )
    {
        return ( List<?> ) syntaxesMap.get( Strings.toLowerCase( id ) );
    }


    /**
     * Adds a SchemaHandlerListener.
     *
     * @param listener
     *      the listener
     */
    public void addListener( SchemaHandlerListener listener )
    {
        if ( !schemaHandlerListeners.contains( listener ) )
        {
            schemaHandlerListeners.add( listener );
        }
    }


    /**
     * Removes a SchemaHandlerListener.
     *
     * @param listener
     *      the listener
     */
    public void removeListener( SchemaHandlerListener listener )
    {
        schemaHandlerListeners.remove( listener );
    }


    /**
     * Adds a schema
     *
     * @param schema
     *      the schema
     */
    public void addSchema( Schema schema )
    {
        // Adding the schema
        schemasList.add( schema );
        schemasMap.put( Strings.toLowerCase( schema.getSchemaName() ), schema );

        // Adding its attribute types
        for ( AttributeType at : schema.getAttributeTypes() )
        {
            addSchemaObject( at );
        }

        // Adding its matching rules
        for ( MatchingRule mr : schema.getMatchingRules() )
        {
            addSchemaObject( mr );
        }

        // Adding its object classes
        for ( ObjectClass oc : schema.getObjectClasses() )
        {
            addSchemaObject( oc );
        }

        // Adding its syntaxes
        for ( LdapSyntax syntax : schema.getSyntaxes() )
        {
            addSchemaObject( syntax );
        }

        notifySchemaAdded( schema );
    }


    /**
     * Adds the given SchemaObject to the corresponding List and Map
     *
     * @param object
     *      the SchemaObject
     */
    private void addSchemaObject( SchemaObject object )
    {
        if ( object instanceof AttributeType )
        {
            AttributeType at = ( AttributeType ) object;
            attributeTypesList.add( at );
            List<String> names = at.getNames();
            if ( names != null )
            {
                for ( String name : names )
                {
                    attributeTypesMap.put( Strings.toLowerCase( name ), at );
                }
            }
            attributeTypesMap.put( at.getOid(), at );
        }
        else if ( object instanceof MatchingRule )
        {
            MatchingRule mr = ( MatchingRule ) object;
            matchingRulesList.add( mr );
            List<String> names = mr.getNames();
            if ( names != null )
            {
                for ( String name : names )
                {
                    matchingRulesMap.put( Strings.toLowerCase( name ), mr );
                }
            }
            matchingRulesMap.put( mr.getOid(), mr );
        }
        else if ( object instanceof ObjectClass )
        {
            ObjectClass oc = ( ObjectClass ) object;
            objectClassesList.add( oc );
            List<String> names = oc.getNames();
            if ( names != null )
            {
                for ( String name : names )
                {
                    objectClassesMap.put( Strings.toLowerCase( name ), oc );
                }
            }
            objectClassesMap.put( oc.getOid(), oc );
        }
        else if ( object instanceof LdapSyntax )
        {
            LdapSyntax syntax = ( LdapSyntax ) object;
            syntaxesList.add( syntax );
            List<String> names = syntax.getNames();
            if ( names != null )
            {
                for ( String name : names )
                {
                    syntaxesMap.put( Strings.toLowerCase( name ), syntax );
                }
            }
            syntaxesMap.put( syntax.getOid(), syntax );
        }
    }


    /**
     * Removes the given schema.
     *
     * @param schema
     *      the schema
     */
    public void removeSchema( Schema schema )
    {
        // Removing the schema
        schemasList.remove( schema );
        schemasMap.remove( Strings.toLowerCase( schema.getSchemaName() ) );

        // Removing its attribute types
        for ( AttributeType at : schema.getAttributeTypes() )
        {
            removeSchemaObject( at );
        }

        // Removing its matching rules
        for ( MatchingRule mr : schema.getMatchingRules() )
        {
            removeSchemaObject( mr );
        }

        // Removing its object classes
        for ( ObjectClass oc : schema.getObjectClasses() )
        {
            removeSchemaObject( oc );
        }

        // Removing its syntaxes
        for ( LdapSyntax syntax : schema.getSyntaxes() )
        {
            removeSchemaObject( syntax );
        }

        notifySchemaRemoved( schema );
    }


    /**
     * Removes the given SchemaObject to the corresponding List and Map
     *
     * @param object
     *      the SchemaObject
     */
    private void removeSchemaObject( SchemaObject object )
    {
        if ( object instanceof AttributeType )
        {
            AttributeType at = ( AttributeType ) object;
            attributeTypesList.remove( at );
            List<String> names = at.getNames();
            if ( names != null )
            {
                for ( String name : names )
                {
                    attributeTypesMap.remove( Strings.toLowerCase( name ) );
                }
            }
            attributeTypesMap.remove( at.getOid() );
        }
        else if ( object instanceof MatchingRule )
        {
            MatchingRule mr = ( MatchingRule ) object;
            matchingRulesList.remove( mr );
            List<String> names = mr.getNames();
            if ( names != null )
            {
                for ( String name : names )
                {
                    matchingRulesMap.remove( Strings.toLowerCase( name ) );
                }
            }
            matchingRulesMap.remove( mr.getOid() );
        }
        else if ( object instanceof ObjectClass )
        {
            ObjectClass oc = ( ObjectClass ) object;
            objectClassesList.remove( oc );
            List<String> names = oc.getNames();
            if ( names != null )
            {
                for ( String name : names )
                {
                    objectClassesMap.remove( Strings.toLowerCase( name ) );
                }
            }
            objectClassesMap.remove( oc.getOid() );
        }
        else if ( object instanceof LdapSyntax )
        {
            LdapSyntax syntax = ( LdapSyntax ) object;
            syntaxesList.remove( syntax );
            List<String> names = syntax.getNames();
            if ( names != null )
            {
                for ( String name : names )
                {
                    syntaxesMap.remove( Strings.toLowerCase( name ) );
                }
            }
            syntaxesMap.remove( syntax.getOid() );
        }
    }


    /**
     * Renames the given schema.
     *
     * @param schema the schema
     * @param newName the new name
     */
    public void renameSchema( Schema schema, String newName )
    {
        schemasMap.remove( Strings.toLowerCase( schema.getSchemaName() ) );
        schema.setSchemaName( newName );
        schemasMap.put( Strings.toLowerCase( schema.getSchemaName() ), schema );

        // Removing its attribute types
        for ( AttributeType at : schema.getAttributeTypes() )
        {
            at.setSchemaName( newName );
        }

        // Removing its matching rules
        for ( MatchingRule mr : schema.getMatchingRules() )
        {
            mr.setSchemaName( newName );
        }

        // Removing its object classes
        for ( ObjectClass oc : schema.getObjectClasses() )
        {
            oc.setSchemaName( newName );
        }

        // Removing its syntaxes
        for ( LdapSyntax syntax : schema.getSyntaxes() )
        {
            syntax.setSchemaName( newName );
        }

        notifySchemaRenamed( schema );
    }


    /**
     * Adds the given attribute type.
     *
     * @param at
     *      the attribute type
     */
    public void addAttributeType( AttributeType at )
    {
        Schema schema = getSchema( at.getSchemaName() );

        schema.addAttributeType( at );
        addSchemaObject( at );

        // Notifying the listeners
        notifyAttributeTypeAdded( at );
    }


    /**
     * Update the source attribute type with the values of the
     * destination attribute type.
     *
     * @param at1
     *      the source attribute type
     * @param at2
     *      the destination attribute type
     */
    public void modifyAttributeType( AttributeType at1, AttributeType at2 )
    {
        // Removing the references (in case of the names or oid have changed)
        removeSchemaObject( at1 );

        // Updating the attribute type
        at1.setNames( at2.getNames() );
        at1.setOid( at2.getOid() );
        at1.setDescription( at2.getDescription() );
        at1.setSuperiorOid( at2.getSuperiorOid() );
        at1.setUsage( at2.getUsage() );
        at1.setSyntaxOid( at2.getSyntaxOid() );
        at1.setSyntaxLength( at2.getSyntaxLength() );
        at1.setObsolete( at2.isObsolete() );
        at1.setSingleValued( at2.isSingleValued() );
        at1.setCollective( at2.isCollective() );
        at1.setUserModifiable( at2.isUserModifiable() );
        at1.setEqualityOid( at2.getEqualityOid() );
        at1.setOrderingOid( at2.getOrderingOid() );
        at1.setSubstringOid( at2.getSubstringOid() );

        // Adding the references (in case of the names or oid have changed)
        addSchemaObject( at1 );

        // Notifying the listeners
        notifyAttributeTypeModified( at1 );
    }


    /**
     * Removes the given attribute type.
     *
     * @param at
     *      the attribute type
     */
    public void removeAttributeType( AttributeType at )
    {
        Schema schema = getSchema( at.getSchemaName() );

        schema.removeAttributeType( at );
        removeSchemaObject( at );

        // Notifying the listeners
        notifyAttributeTypeRemoved( at );
    }


    /**
     * Adds the given object class.
     *
     * @param oc
     *      the object class
     */
    public void addObjectClass( ObjectClass oc )
    {
        Schema schema = getSchema( oc.getSchemaName() );

        schema.addObjectClass( oc );
        addSchemaObject( oc );

        // Notifying the listeners
        notifyObjectClassAdded( oc );
    }


    /**
     * Update the source object class with the values of the
     * destination object class.
     *
     * @param oc1
     *      the source object class
     * @param oc2
     *      the destination object class
     */
    public void modifyObjectClass( ObjectClass oc1, ObjectClass oc2 )
    {
        // Removing the references (in case of the names or oid have changed)
        removeSchemaObject( oc1 );

        // Updating the object class
        oc1.setNames( oc2.getNames() );
        oc1.setOid( oc2.getOid() );
        oc1.setDescription( oc2.getDescription() );
        oc1.setSuperiorOids( oc2.getSuperiorOids() );
        oc1.setType( oc2.getType() );
        oc1.setObsolete( oc2.isObsolete() );
        oc1.setMustAttributeTypeOids( oc2.getMustAttributeTypeOids() );
        oc1.setMayAttributeTypeOids( oc2.getMayAttributeTypeOids() );

        // Adding the references (in case of the names or oid have changed)
        addSchemaObject( oc1 );

        // Notifying the listeners
        notifyObjectClassModified( oc1 );
    }


    /**
     * Removes the given object class.
     *
     * @param oc
     *      the object class
     */
    public void removeObjectClass( ObjectClass oc )
    {
        Schema schema = getSchema( oc.getSchemaName() );

        schema.removeObjectClass( oc );
        removeSchemaObject( oc );

        notifyObjectClassRemoved( oc );
    }


    /**
     * Notifies the SchemaHandler listeners that a schema has been added.
     *
     * @param schema
     *      the added schema
     */
    private void notifySchemaAdded( Schema schema )
    {
        for ( SchemaHandlerListener listener : schemaHandlerListeners.toArray( new SchemaHandlerListener[0] ) )
        {
            listener.schemaAdded( schema );
        }
    }


    /**
     * Notifies the given listeners that a schema has been removed.
     *
     * @param schema
     *      the removed schema
     */
    private void notifySchemaRemoved( Schema schema )
    {
        for ( SchemaHandlerListener listener : schemaHandlerListeners.toArray( new SchemaHandlerListener[0] ) )
        {
            listener.schemaRemoved( schema );
        }
    }


    /**
     * Notifies the given listeners that a schema has been renamed.
     *
     * @param schema
     *      the renamed schema
     */
    private void notifySchemaRenamed( Schema schema )
    {
        for ( SchemaHandlerListener listener : schemaHandlerListeners.toArray( new SchemaHandlerListener[0] ) )
        {
            listener.schemaRenamed( schema );
        }
    }


    /**
     * Notifies the SchemaHandler listeners that an attribute type has been added.
     *
     * @param at
     *      the added attribute type
     */
    private void notifyAttributeTypeAdded( AttributeType at )
    {
        // SchemaHandler Listeners
        for ( SchemaHandlerListener listener : schemaHandlerListeners.toArray( new SchemaHandlerListener[0] ) )
        {
            listener.attributeTypeAdded( at );
        }
    }


    /**
     * Notifies the SchemaHandler listeners that an attribute type has been modified.
     *
     * @param at
     *      the modified attribute type
     */
    private void notifyAttributeTypeModified( AttributeType at )
    {
        // SchemaHandler Listeners
        for ( SchemaHandlerListener listener : schemaHandlerListeners.toArray( new SchemaHandlerListener[0] ) )
        {
            listener.attributeTypeModified( at );
        }
    }


    /**
     * Notifies the SchemaHandler listeners that an attribute type has been removed.
     *
     * @param at
     *      the removed attribute type
     */
    private void notifyAttributeTypeRemoved( AttributeType at )
    {
        // SchemaHandler Listeners
        for ( SchemaHandlerListener listener : schemaHandlerListeners.toArray( new SchemaHandlerListener[0] ) )
        {
            listener.attributeTypeRemoved( at );
        }
    }


    /**
     * Notifies the SchemaHandler listeners that an object class has been added.
     *
     * @param oc
     *      the added object class
     */
    private void notifyObjectClassAdded( ObjectClass oc )
    {
        // SchemaHandler Listeners
        for ( SchemaHandlerListener listener : schemaHandlerListeners.toArray( new SchemaHandlerListener[0] ) )
        {
            listener.objectClassAdded( oc );
        }
    }


    /**
     * Notifies the SchemaHandler listeners that an object class has been modified.
     *
     * @param oc
     *      the modified object class
     */
    private void notifyObjectClassModified( ObjectClass oc )
    {
        // SchemaHandler Listeners
        for ( SchemaHandlerListener listener : schemaHandlerListeners.toArray( new SchemaHandlerListener[0] ) )
        {
            listener.objectClassModified( oc );
        }
    }


    /**
     * Notifies the SchemaHandler listeners that an object class has been removed.
     *
     * @param oc
     *      the removed object class
     */
    private void notifyObjectClassRemoved( ObjectClass oc )
    {
        // SchemaHandler Listeners
        for ( SchemaHandlerListener listener : schemaHandlerListeners.toArray( new SchemaHandlerListener[0] ) )
        {
            listener.objectClassRemoved( oc );
        }
    }


    /**
     * Verifies if the given oid is already taken by a schema object.
     *
     * @param oid the oid
     * @return <code>true</code> if the the oid is already taken
     */
    public boolean isOidAlreadyTaken( String oid )
    {
        String lowerCasedOid = Strings.toLowerCase( oid );
        if ( attributeTypesMap.containsKey( lowerCasedOid ) )
        {
            return true;
        }
        else if ( objectClassesMap.containsKey( lowerCasedOid ) )
        {
            return true;
        }
        else if ( matchingRulesMap.containsKey( lowerCasedOid ) )
        {
            return true;
        }
        else if ( syntaxesMap.containsKey( lowerCasedOid ) )
        {
            return true;
        }

        return false;
    }


    /**
     * Verifies if the given alias is already taken by an attribute type.
     *
     * @param alias the alias
     * @return <code>true</code> if the the alias is already taken
     */
    public boolean isAliasAlreadyTakenForAttributeType( String alias )
    {
        return attributeTypesMap.containsKey( Strings.toLowerCase( alias ) );
    }


    /**
     * Verifies if the given alias is already taken by an object class.
     *
     * @param alias the alias
     * @return <code>true</code> if the the alias is already taken
     */
    public boolean isAliasAlreadyTakenForObjectClass( String alias )
    {
        return objectClassesMap.containsKey( Strings.toLowerCase( alias ) );
    }


    /**
     * Verifies if the given name for a schema is already taken by another schema.
     *
     * @param name
     *      the name
     * @return
     *      true if the the name is already taken
     */
    public boolean isSchemaNameAlreadyTaken( String name )
    {
        return schemasMap.containsKey( Strings.toLowerCase( name ) );
    }
}
