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


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.exception.LdapSchemaException;
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.SchemaManager;
import org.apache.directory.api.ldap.model.schema.SchemaObject;
import org.apache.directory.api.ldap.schema.manager.impl.DefaultSchemaManager;
import org.apache.directory.studio.schemaeditor.Activator;
import org.apache.directory.studio.schemaeditor.controller.ProjectsHandlerAdapter;
import org.apache.directory.studio.schemaeditor.controller.SchemaHandler;
import org.apache.directory.studio.schemaeditor.controller.SchemaHandlerAdapter;
import org.apache.directory.studio.schemaeditor.controller.SchemaHandlerListener;
import org.apache.directory.studio.schemaeditor.model.Project;
import org.apache.directory.studio.schemaeditor.model.Schema;
import org.apache.directory.studio.schemaeditor.model.schemamanager.SchemaEditorSchemaLoader;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;


/**
 * This class represents the SchemaChecker.
 * <p>
 * It is used to check the schema integrity.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class SchemaChecker
{
    /** The SchemaChecker instance */
    private static SchemaChecker instance;

    /** The schema manager */
    private SchemaManager schemaManager;

    /** The errors map */
    private MultiValuedMap<Object, Object> errorsMap = new ArrayListValuedHashMap<>();

    /** The warnings list */
    private List<SchemaWarning> warningsList = new ArrayList<SchemaWarning>();

    /** The warnings map */
    private MultiValuedMap<Object, Object> warningsMap = new ArrayListValuedHashMap<>();

    /** The lock object used to synchronize accesses to the errors and warnings maps*/
    private static Object lock = new Object();

    /** The 'listening to modifications' flag*/
    private boolean listeningToModifications = false;

    /** The listeners List */
    private List<SchemaCheckerListener> listeners = new ArrayList<SchemaCheckerListener>();

    /** The SchemaHandlerListener */
    private SchemaHandlerListener schemaHandlerListener = new SchemaHandlerAdapter()
    {
        public void attributeTypeAdded( AttributeType at )
        {
            synchronized ( this )
            {
                recheckWholeSchema();
            }
        }


        public void attributeTypeModified( AttributeType at )
        {
            synchronized ( this )
            {
                recheckWholeSchema();
            }
        }


        public void attributeTypeRemoved( AttributeType at )
        {
            synchronized ( this )
            {
                recheckWholeSchema();
            }
        }


        public void objectClassAdded( ObjectClass oc )
        {
            synchronized ( this )
            {
                recheckWholeSchema();
            }
        }


        public void objectClassModified( ObjectClass oc )
        {
            synchronized ( this )
            {
                recheckWholeSchema();
            }
        }


        public void objectClassRemoved( ObjectClass oc )
        {
            synchronized ( this )
            {
                recheckWholeSchema();
            }
        }


        public void schemaAdded( Schema schema )
        {
            synchronized ( this )
            {
                recheckWholeSchema();
            }
        }


        public void schemaRemoved( Schema schema )
        {
            synchronized ( this )
            {
                recheckWholeSchema();
            }
        }


        public void schemaRenamed( Schema schema )
        {
            // Nothing to do, this is a simple renaming
        }
    };


    /**
     * Creates a new instance of SchemaChecker.
     */
    private SchemaChecker()
    {
        Activator.getDefault().getProjectsHandler().addListener( new ProjectsHandlerAdapter()
        {
            public void openProjectChanged( Project oldProject, Project newProject )
            {
                if ( oldProject != null )
                {
                    oldProject.getSchemaHandler().removeListener( schemaHandlerListener );
                }

                if ( newProject != null )
                {
                    newProject.getSchemaHandler().addListener( schemaHandlerListener );
                }
            }
        } );
    }


    /**
     * Gets the singleton instance of the ProjectsHandler.
     *
     * @return
     *      the singleton instance of the ProjectsHandler
     */
    public static SchemaChecker getInstance()
    {
        if ( instance == null )
        {
            instance = new SchemaChecker();
        }

        return instance;
    }


    /**
     * Enables modifications listening.
     */
    public void enableModificationsListening()
    {
        synchronized ( this )
        {
            if ( !listeningToModifications )
            {
                Activator.getDefault().getSchemaHandler().addListener( schemaHandlerListener );
                listeningToModifications = true;
                recheckWholeSchema();
            }
        }
    }


    /**
     * Disables modifications listening.
     */
    public void disableModificationsListening()
    {
        synchronized ( this )
        {
            if ( listeningToModifications )
            {
                Activator.getDefault().getSchemaHandler().removeListener( schemaHandlerListener );
                listeningToModifications = false;
            }
        }
    }


    /**
     * Reloads the content of the schema checker
     */
    public void reload()
    {
        synchronized ( this )
        {
            recheckWholeSchema();
        }
    }


    /**
     * Returns true if the SchemaChecker is listening to modifications, 
     * false if not.
     *
     * @return
     *      true if the SchemaChecker is listening to modifications, 
     * false if not
     */
    public boolean isListeningToModifications()
    {
        return listeningToModifications;
    }


    /**
     * Checks the whole schema.
     */
    private void recheckWholeSchema()
    {
        Job job = new Job( "Checking Schema" )
        {
            protected IStatus run( IProgressMonitor monitor )
            {
                // Checks the whole schema via the schema manager
                try
                {
                    schemaManager = new DefaultSchemaManager( new SchemaEditorSchemaLoader() );
                    schemaManager.loadAllEnabled();
                }
                catch ( Exception e )
                {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                // Updates errors and warnings
                updateErrorsAndWarnings();

                // Notify listeners
                notifyListeners();

                monitor.done();

                return Status.OK_STATUS;
            }
        };
        job.schedule();
    }


    /**
     * Updates the errors and warnings. 
     */
    private synchronized void updateErrorsAndWarnings()
    {
        synchronized ( lock )
        {
            // Errors
            errorsMap.clear();
            indexErrors();

            // Warnings
            createWarnings();
            warningsMap.clear();
            indexWarnings();
        }
    }


    /**
     * Indexes the errors.
     */
    private void indexErrors()
    {
        for ( Throwable error : schemaManager.getErrors() )
        {
            if ( error instanceof LdapSchemaException )
            {
                LdapSchemaException ldapSchemaException = ( LdapSchemaException ) error;
                SchemaObject source = ldapSchemaException.getSourceObject();
                if ( source != null )
                {
                    SchemaHandler schemaHandler = Activator.getDefault().getSchemaHandler();

                    if ( source instanceof AttributeType )
                    {
                        source = schemaHandler.getAttributeType( source.getOid() );
                    }
                    else if ( source instanceof LdapSyntax )
                    {
                        source = schemaHandler.getSyntax( source.getOid() );
                    }
                    else if ( source instanceof MatchingRule )
                    {
                        source = schemaHandler.getMatchingRule( source.getOid() );
                    }
                    else if ( source instanceof ObjectClass )
                    {
                        source = schemaHandler.getObjectClass( source.getOid() );
                    }

                    errorsMap.put( source, ldapSchemaException );
                }
            }
        }
    }


    /**
     * Creates the warnings.
     */
    private void createWarnings()
    {
        // Clearing previous warnings
        warningsList.clear();

        // Getting the schema handler to check for schema objects without names (aliases)
        SchemaHandler schemaHandler = Activator.getDefault().getSchemaHandler();

        if ( schemaHandler != null )
        {
            // Checking attribute types
            for ( AttributeType attributeType : schemaHandler.getAttributeTypes() )
            {
                checkSchemaObjectNames( attributeType );
            }

            // Checking object classes
            for ( ObjectClass objectClass : schemaHandler.getObjectClasses() )
            {
                checkSchemaObjectNames( objectClass );
            }
        }
    }


    /**
     * Checks the names of the given schema object.
     *
     * @param schemaObject the schema object to check
     */
    private void checkSchemaObjectNames( SchemaObject schemaObject )
    {
        if ( ( schemaObject.getNames() == null ) || ( schemaObject.getNames().size() == 0 ) )
        {
            warningsList.add( new NoAliasWarning( schemaObject ) );
        }
    }


    /**
     * Indexes the warnings.
     */
    private void indexWarnings()
    {
        for ( SchemaWarning warning : warningsList )
        {
            warningsMap.put( warning.getSource(), warning );
        }
    }


    /**
     * Gets the errors.
     *
     * @return
     *      the errors
     */
    public List<Throwable> getErrors()
    {
        if ( schemaManager != null )
        {
            return schemaManager.getErrors();
        }
        else
        {
            return new ArrayList<Throwable>();
        }
    }


    /**
     * Gets the warnings.
     *
     * @return
     *      the warnings
     */
    public List<SchemaWarning> getWarnings()
    {
        synchronized ( lock )
        {
            return warningsList;
        }
    }


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


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


    /**
     * Notifies the listeners.
     */
    private void notifyListeners()
    {
        for ( SchemaCheckerListener listener : listeners )
        {
            listener.schemaCheckerUpdated();
        }
    }


    /**
     * Gets the errors associated with the given Schema Object
     *
     * @param so
     *      the Schema Object
     * @return
     *      the associated errors
     */
    public List<?> getErrors( SchemaObject so )
    {
        synchronized ( lock )
        {
            return ( List<?> ) errorsMap.get( so );
        }
    }


    /**
     * Returns whether the given Schema Object has errors.
     *
     * @param so
     *      the Schema Object
     * @return
     *      true if the given Schema Object has errors.
     */
    public boolean hasErrors( SchemaObject so )
    {
        List<?> errors = getErrors( so );

        if ( errors == null )
        {
            return false;
        }
        else
        {
            return errors.size() > 0;
        }
    }


    /**
     * Gets the warnings associated with the given Schema Object
     *
     * @param so
     *      the Schema Object
     * @return
     *      the associated warnings
     */
    @SuppressWarnings("unchecked")
    public List<Object> getWarnings( SchemaObject so )
    {
        return ( List<Object> ) warningsMap.get( so );
    }


    /**
     * Returns whether the given Schema Object has warnings.
     *
     * @param so
     *      the Schema Object
     * @return
     *      true if the given Schema Object has errors.
     */
    public boolean hasWarnings( SchemaObject so )
    {
        List<?> warnings = getWarnings( so );

        if ( warnings == null )
        {
            return false;
        }
        else
        {
            return warnings.size() > 0;
        }
    }

}
