/*
 *  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.view.wizards;


import java.util.ArrayList;
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.ldap.model.schema.AbstractSchemaObject;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.ObjectClass;
import org.apache.directory.studio.common.ui.dialogs.MessageDialogWithTextarea;
import org.apache.directory.studio.schemaeditor.Activator;
import org.apache.directory.studio.schemaeditor.model.Project;
import org.apache.directory.studio.schemaeditor.model.Schema;
import org.apache.directory.studio.schemaeditor.view.wizards.MergeSchemasSelectionWizardPage.AttributeTypeFolder;
import org.apache.directory.studio.schemaeditor.view.wizards.MergeSchemasSelectionWizardPage.AttributeTypeWrapper;
import org.apache.directory.studio.schemaeditor.view.wizards.MergeSchemasSelectionWizardPage.ObjectClassFolder;
import org.apache.directory.studio.schemaeditor.view.wizards.MergeSchemasSelectionWizardPage.ObjectClassWrapper;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.wizard.Wizard;
import org.eclipse.osgi.util.NLS;
import org.eclipse.ui.IImportWizard;
import org.eclipse.ui.IWorkbench;


/**
 * This class represents the wizard to merge schema projects.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class MergeSchemasWizard extends Wizard implements IImportWizard
{
    // The pages of the wizard
    private MergeSchemasSelectionWizardPage selectionPage;
    private MergeSchemasOptionsWizardPage optionsPage;


    /**
     * {@inheritDoc}
     */
    public void addPages()
    {
        // Creating pages
        selectionPage = new MergeSchemasSelectionWizardPage();
        optionsPage = new MergeSchemasOptionsWizardPage();

        // Adding pages
        addPage( selectionPage );
        addPage( optionsPage );
    }


    /**
     * {@inheritDoc}
     */
    public boolean performFinish()
    {
        Object[] sourceObjects = selectionPage.getSelectedObjects();

        boolean replaceUnknownSyntax = optionsPage.isReplaceUnknownSyntax();
        boolean mergeDependencies = optionsPage.isMergeDependencies();
        boolean pullUpAttributes = optionsPage.isPullUpAttributes();

        List<String> errorMessages = new ArrayList<String>();
        mergeObjects( sourceObjects, errorMessages, replaceUnknownSyntax, mergeDependencies, pullUpAttributes );
        if ( !errorMessages.isEmpty() )
        {
            StringBuilder sb = new StringBuilder();
            for ( String errorMessage : errorMessages )
            {
                sb.append( errorMessage );
                sb.append( '\n' );
            }
            new MessageDialogWithTextarea( getShell(), Messages.getString( "MergeSchemasWizard.MergeResultTitle" ), //$NON-NLS-1$
                Messages.getString( "MergeSchemasWizard.MergeResultMessage" ), sb.toString() ).open(); //$NON-NLS-1$
        }

        return true;
    }


    private void mergeObjects( Object[] sourceObjects, List<String> errorMessages, boolean replaceUnknownSyntax,
        boolean mergeDependencies, boolean pullUpAttributes )
    {
        /*
         * List of already processed schema objects. Used to avoid that schema objects are process multiple time.
         */
        Set<Object> processedObjects = new HashSet<Object>();

        /*
         * List of created target schemas.
         */
        Map<String, Schema> targetSchemas = new HashMap<String, Schema>();

        Project targetProject = Activator.getDefault().getProjectsHandler().getOpenProject();

        // merge all source objects to the target project
        for ( Object sourceObject : sourceObjects )
        {
            if ( sourceObject instanceof Project )
            {
                Project sourceProject = ( Project ) sourceObject;
                for ( Schema sourceSchema : sourceProject.getSchemaHandler().getSchemas() )
                {
                    Schema targetSchema = getTargetSchema( sourceSchema.getProject(), targetProject, targetSchemas );
                    mergeSchema( sourceSchema, targetProject, targetSchema, processedObjects, errorMessages,
                        replaceUnknownSyntax, mergeDependencies, pullUpAttributes );
                }
            }
            if ( sourceObject instanceof Schema )
            {
                Schema sourceSchema = ( Schema ) sourceObject;
                Schema targetSchema = getTargetSchema( sourceSchema.getProject(), targetProject, targetSchemas );
                mergeSchema( sourceSchema, targetProject, targetSchema, processedObjects, errorMessages,
                    replaceUnknownSyntax, mergeDependencies, pullUpAttributes );
            }
            if ( sourceObject instanceof AttributeTypeFolder )
            {
                AttributeTypeFolder atf = ( AttributeTypeFolder ) sourceObject;
                Schema targetSchema = getTargetSchema( atf.schema.getProject(), targetProject, targetSchemas );
                List<AttributeType> sourceAttributeTypes = atf.schema.getAttributeTypes();
                for ( AttributeType sourceAttributeType : sourceAttributeTypes )
                {
                    mergeAttributeType( sourceAttributeType, targetProject, targetSchema, processedObjects,
                        errorMessages, replaceUnknownSyntax, mergeDependencies, pullUpAttributes );
                }
            }
            if ( sourceObject instanceof ObjectClassFolder )
            {
                ObjectClassFolder ocf = ( ObjectClassFolder ) sourceObject;
                Schema targetSchema = getTargetSchema( ocf.schema.getProject(), targetProject, targetSchemas );
                List<ObjectClass> sourceObjectClasses = ocf.schema.getObjectClasses();
                for ( ObjectClass sourceObjectClass : sourceObjectClasses )
                {
                    mergeObjectClass( sourceObjectClass, targetProject, targetSchema, processedObjects, errorMessages,
                        replaceUnknownSyntax, mergeDependencies, pullUpAttributes );
                }
            }
            if ( sourceObject instanceof AttributeTypeWrapper )
            {
                AttributeTypeWrapper atw = ( AttributeTypeWrapper ) sourceObject;
                Schema targetSchema = getTargetSchema( atw.folder.schema.getProject(), targetProject, targetSchemas );
                mergeAttributeType( atw.attributeType, targetProject, targetSchema, processedObjects, errorMessages,
                    replaceUnknownSyntax, mergeDependencies, pullUpAttributes );
            }
            if ( sourceObject instanceof ObjectClassWrapper )
            {
                ObjectClassWrapper ocw = ( ObjectClassWrapper ) sourceObject;
                Schema targetSchema = getTargetSchema( ocw.folder.schema.getProject(), targetProject, targetSchemas );
                mergeObjectClass( ocw.objectClass, targetProject, targetSchema, processedObjects, errorMessages,
                    replaceUnknownSyntax, mergeDependencies, pullUpAttributes );
            }
        }

        //add created target schemas to project
        for ( Schema targetSchema : targetSchemas.values() )
        {
            if ( !targetProject.getSchemaHandler().getSchemas().contains( targetSchema ) )
            {
                targetProject.getSchemaHandler().addSchema( targetSchema );
            }
        }
    }


    private Schema getTargetSchema( Project sourceProject, Project targetProject, Map<String, Schema> targetSchemas )
    {
        String targetSchemaName = "merge-from-" + sourceProject.getName(); //$NON-NLS-1$
        Schema targetSchema = targetProject.getSchemaHandler().getSchema( targetSchemaName );
        if ( targetSchema != null )
        {
            targetProject.getSchemaHandler().removeSchema( targetSchema );
        }
        else if ( targetSchemas.containsKey( targetSchemaName ) )
        {
            targetSchema = targetSchemas.get( targetSchemaName );
        }
        else
        {
            targetSchema = new Schema( targetSchemaName );
            targetSchema.setProject( targetProject );
        }
        targetSchemas.put( targetSchemaName, targetSchema );
        return targetSchema;
    }


    /**
     * Merges all attribute types and object classes and form the given sourceSchema to the targetSchema.
     */
    private void mergeSchema( Schema sourceSchema, Project targetProject, Schema targetSchema,
        Set<Object> processedObjects, List<String> errorMessages, boolean replaceUnknownSyntax,
        boolean mergeDependencies, boolean pullUpAttributes )
    {
        List<AttributeType> sourceAttributeTypes = sourceSchema.getAttributeTypes();
        for ( AttributeType sourceAttributeType : sourceAttributeTypes )
        {
            mergeAttributeType( sourceAttributeType, targetProject, targetSchema, processedObjects, errorMessages,
                replaceUnknownSyntax, mergeDependencies, pullUpAttributes );
        }

        List<ObjectClass> sourceObjectClasses = sourceSchema.getObjectClasses();
        for ( ObjectClass sourceObjectClass : sourceObjectClasses )
        {
            mergeObjectClass( sourceObjectClass, targetProject, targetSchema, processedObjects, errorMessages,
                replaceUnknownSyntax, mergeDependencies, pullUpAttributes );
        }
    }


    /**
     * Merges the given attribute type to the targetSchema.
     */
    private void mergeAttributeType( AttributeType sourceAttributeType, Project targetProject, Schema targetSchema,
        Set<Object> processedObjects, List<String> errorMessages, boolean replaceUnknownSyntax,
        boolean mergeDependencies, boolean pullUpAttributes )
    {
        if ( processedObjects.contains( sourceAttributeType ) )
        {
            return;
        }
        processedObjects.add( sourceAttributeType );

        // check if attribute (identified by OID or name) already exists in the project
        AttributeType targetAttributeType = targetProject.getSchemaHandler().getAttributeType(
            sourceAttributeType.getOid() );
        if ( targetAttributeType == null )
        {
            for ( String name : sourceAttributeType.getNames() )
            {
                targetAttributeType = targetProject.getSchemaHandler().getAttributeType( name );
                if ( targetAttributeType != null )
                {
                    break;
                }
            }
        }

        // check if OID or alias name already exist in target project
        boolean oidOrAliasAlreadyTaken = targetProject.getSchemaHandler().isOidAlreadyTaken(
            sourceAttributeType.getOid() );
        if ( !oidOrAliasAlreadyTaken )
        {
            for ( String name : sourceAttributeType.getNames() )
            {
                oidOrAliasAlreadyTaken = targetProject.getSchemaHandler().isAliasAlreadyTakenForAttributeType( name );
                if ( oidOrAliasAlreadyTaken )
                {
                    break;
                }
            }
        }

        if ( targetAttributeType != null )
        {
            errorMessages.add( NLS.bind( Messages.getString( "MergeSchemasWizard.AttributeTypeExistsInTargetProject" ), //$NON-NLS-1$
                getIdString( sourceAttributeType ) ) );
        }
        else
        {
            if ( oidOrAliasAlreadyTaken )
            {
                errorMessages.add( NLS.bind( Messages.getString( "MergeSchemasWizard.OidOrAliasAlreadyTaken" ), //$NON-NLS-1$
                    getIdString( sourceAttributeType ) ) );
            }
            else
            {
                // remove attribute type if already there from previous merge
                AttributeType at = targetSchema.getAttributeType( sourceAttributeType.getOid() );
                if ( at != null )
                {
                    targetSchema.removeAttributeType( at );
                }

                // clone attribute type
                AttributeType clonedAttributeType = new AttributeType( sourceAttributeType.getOid() );
                clonedAttributeType.setNames( sourceAttributeType.getNames() );
                clonedAttributeType.setDescription( sourceAttributeType.getDescription() );
                clonedAttributeType.setSuperiorOid( sourceAttributeType.getSuperiorOid() );
                clonedAttributeType.setUsage( sourceAttributeType.getUsage() );
                clonedAttributeType.setSyntaxOid( sourceAttributeType.getSyntaxOid() );
                clonedAttributeType.setSyntaxLength( sourceAttributeType.getSyntaxLength() );
                clonedAttributeType.setObsolete( sourceAttributeType.isObsolete() );
                clonedAttributeType.setCollective( sourceAttributeType.isCollective() );
                clonedAttributeType.setSingleValued( sourceAttributeType.isSingleValued() );
                clonedAttributeType.setUserModifiable( sourceAttributeType.isUserModifiable() );
                clonedAttributeType.setEqualityOid( sourceAttributeType.getEqualityOid() );
                clonedAttributeType.setOrderingOid( sourceAttributeType.getOrderingOid() );
                clonedAttributeType.setSubstringOid( sourceAttributeType.getSubstringOid() );
                clonedAttributeType.setSchemaName( targetSchema.getSchemaName() );

                // if no/unknown syntax: set "Directory String" syntax and appropriate matching rules
                if ( replaceUnknownSyntax )
                {
                    if ( clonedAttributeType.getSyntaxOid() == null
                        || targetProject.getSchemaHandler().getSyntax( clonedAttributeType.getSyntaxOid() ) == null )
                    {
                        errorMessages.add( NLS.bind( Messages.getString( "MergeSchemasWizard.ReplacedSyntax" ), //$NON-NLS-1$
                            new String[]
                                {
                                    getIdString( sourceAttributeType ),
                                    clonedAttributeType.getSyntaxOid(),
                                    "1.3.6.1.4.1.1466.115.121.1.15 (Directory String)" } ) ); //$NON-NLS-1$
                        clonedAttributeType.setSyntaxOid( "1.3.6.1.4.1.1466.115.121.1.15" ); //$NON-NLS-1$
                        clonedAttributeType.setEqualityOid( "caseIgnoreMatch" ); //$NON-NLS-1$
                        clonedAttributeType.setOrderingOid( null );
                        clonedAttributeType.setSubstringOid( "caseIgnoreSubstringsMatch" ); //$NON-NLS-1$
                    }
                }
                // TODO: if unknown (single) matching rule: set appropriate matching rule according to syntax
                // TODO: if no (all) matching rules: set appropriate matching rules according to syntax

                // merge dependencies: super attribute type
                if ( mergeDependencies )
                {
                    String superiorName = clonedAttributeType.getSuperiorOid();
                    if ( superiorName != null )
                    {
                        AttributeType superiorAttributeType = Activator.getDefault().getSchemaHandler()
                            .getAttributeType( superiorName );
                        if ( superiorAttributeType != null )
                        {
                            mergeAttributeType( superiorAttributeType, targetProject, targetSchema, processedObjects,
                                errorMessages, replaceUnknownSyntax, mergeDependencies, pullUpAttributes );
                        }
                    }
                }

                targetSchema.addAttributeType( clonedAttributeType );
            }
        }
    }


    /**
     * Merges the given object class to the targetSchema.
     */
    private void mergeObjectClass( ObjectClass sourceObjectClass, Project targetProject, Schema targetSchema,
        Set<Object> processedObjects, List<String> errorMessages, boolean replaceUnknownSyntax,
        boolean mergeDependencies, boolean pullUpAttributes )
    {
        if ( processedObjects.contains( sourceObjectClass ) )
        {
            return;
        }
        processedObjects.add( sourceObjectClass );

        // check if object class (identified by OID or alias name) already exists in the target project
        ObjectClass targetObjectClass = targetProject.getSchemaHandler()
            .getObjectClass( sourceObjectClass.getOid() );
        if ( targetObjectClass == null )
        {
            for ( String name : sourceObjectClass.getNames() )
            {
                targetObjectClass = targetProject.getSchemaHandler().getObjectClass( name );
                if ( targetObjectClass != null )
                {
                    break;
                }
            }
        }

        // check if OID or alias name already exist in target project
        boolean oidOrAliasAlreadyTaken = targetProject.getSchemaHandler().isOidAlreadyTaken(
            sourceObjectClass.getOid() );
        if ( !oidOrAliasAlreadyTaken )
        {
            for ( String name : sourceObjectClass.getNames() )
            {
                oidOrAliasAlreadyTaken = targetProject.getSchemaHandler().isAliasAlreadyTakenForObjectClass( name );
                if ( oidOrAliasAlreadyTaken )
                {
                    break;
                }
            }
        }

        if ( targetObjectClass != null )
        {
            errorMessages.add( NLS.bind( Messages.getString( "MergeSchemasWizard.ObjectClassExistsInTargetProject" ), //$NON-NLS-1$
                getIdString( sourceObjectClass ) ) );
        }
        else
        {
            if ( oidOrAliasAlreadyTaken )
            {
                errorMessages.add( NLS.bind( Messages.getString( "MergeSchemasWizard.OidOrAliasAlreadyTaken" ), //$NON-NLS-1$
                    getIdString( sourceObjectClass ) ) );
            }
            else
            {
                // remove object class if already there from previous merge
                ObjectClass oc = targetSchema.getObjectClass( sourceObjectClass.getOid() );
                if ( oc != null )
                {
                    targetSchema.removeObjectClass( oc );
                }

                // create object class
                ObjectClass clonedObjectClass = new ObjectClass( sourceObjectClass.getOid() );
                clonedObjectClass.setOid( sourceObjectClass.getOid() );
                clonedObjectClass.setNames( sourceObjectClass.getNames() );
                clonedObjectClass.setDescription( sourceObjectClass.getDescription() );
                clonedObjectClass.setSuperiorOids( sourceObjectClass.getSuperiorOids() );
                clonedObjectClass.setType( sourceObjectClass.getType() );
                clonedObjectClass.setObsolete( sourceObjectClass.isObsolete() );
                clonedObjectClass.setMustAttributeTypeOids( sourceObjectClass.getMustAttributeTypeOids() );
                clonedObjectClass.setMayAttributeTypeOids( sourceObjectClass.getMayAttributeTypeOids() );
                clonedObjectClass.setSchemaName( targetSchema.getSchemaName() );

                // merge dependencies: super object classes and must/may attributes
                if ( mergeDependencies )
                {
                    List<String> superClassesNames = clonedObjectClass.getSuperiorOids();
                    if ( superClassesNames != null )
                    {
                        for ( String superClassName : superClassesNames )
                        {
                            if ( superClassName != null )
                            {
                                ObjectClass superSourceObjectClass = Activator.getDefault().getSchemaHandler()
                                    .getObjectClass( superClassName );
                                ObjectClass superTargetObjectClass = targetProject.getSchemaHandler()
                                    .getObjectClass( superClassName );
                                if ( superSourceObjectClass != null )
                                {
                                    if ( superTargetObjectClass == null )
                                    {
                                        mergeObjectClass( superSourceObjectClass, targetProject, targetSchema,
                                            processedObjects, errorMessages, replaceUnknownSyntax, mergeDependencies,
                                            pullUpAttributes );
                                    }
                                    else
                                    {
                                        // pull-up may and must attributes to this OC if super already exists in target
                                        if ( pullUpAttributes )
                                        {
                                            pullUpAttributes( clonedObjectClass, superSourceObjectClass,
                                                superTargetObjectClass );
                                        }
                                    }
                                }
                            }
                        }
                    }

                    List<String> mustNamesList = clonedObjectClass.getMustAttributeTypeOids();
                    List<String> mayNamesList = clonedObjectClass.getMayAttributeTypeOids();
                    List<String> attributeNames = new ArrayList<String>();
                    if ( mustNamesList != null )
                    {
                        attributeNames.addAll( mustNamesList );
                    }
                    if ( mayNamesList != null )
                    {
                        attributeNames.addAll( mayNamesList );
                    }
                    for ( String attributeName : attributeNames )
                    {
                        if ( attributeName != null )
                        {
                            AttributeType attributeType = Activator.getDefault().getSchemaHandler()
                                .getAttributeType( attributeName );
                            if ( attributeType != null )
                            {
                                mergeAttributeType( attributeType, targetProject, targetSchema, processedObjects,
                                    errorMessages, replaceUnknownSyntax, mergeDependencies, pullUpAttributes );
                            }
                        }
                    }
                }

                targetSchema.addObjectClass( clonedObjectClass );
            }
        }
    }


    private void pullUpAttributes( ObjectClass targetObjectClass, ObjectClass sourceSuperObjectClass,
        ObjectClass targetSuperObjectClass )
    {
        // must
        Set<String> sourceMustAttributeNames = new HashSet<String>();
        fetchAttributes( sourceMustAttributeNames, sourceSuperObjectClass, true );
        Set<String> targetMustAttributeNames = new HashSet<String>();
        fetchAttributes( targetMustAttributeNames, targetSuperObjectClass, true );
        sourceMustAttributeNames.removeAll( targetMustAttributeNames );
        if ( !sourceMustAttributeNames.isEmpty() )
        {
            sourceMustAttributeNames.addAll( targetObjectClass.getMustAttributeTypeOids() );
            targetObjectClass.setMustAttributeTypeOids( new ArrayList<String>( sourceMustAttributeNames ) );
        }

        // may
        Set<String> sourceMayAttributeNames = new HashSet<String>();
        fetchAttributes( sourceMayAttributeNames, sourceSuperObjectClass, false );
        Set<String> targetMayAttributeNames = new HashSet<String>();
        fetchAttributes( targetMayAttributeNames, targetSuperObjectClass, false );
        sourceMayAttributeNames.removeAll( targetMayAttributeNames );
        if ( !sourceMayAttributeNames.isEmpty() )
        {
            sourceMayAttributeNames.addAll( targetObjectClass.getMayAttributeTypeOids() );
            targetObjectClass.setMayAttributeTypeOids( new ArrayList<String>( sourceMayAttributeNames ) );
        }
    }


    private void fetchAttributes( Set<String> attributeNameList, ObjectClass oc, boolean must )
    {
        List<String> attributeNames = must ? oc.getMustAttributeTypeOids() : oc.getMayAttributeTypeOids();
        attributeNameList.addAll( attributeNames );

        for ( String superClassName : oc.getSuperiorOids() )
        {
            ObjectClass superObjectClass = Activator.getDefault().getSchemaHandler().getObjectClass(
                superClassName );
            fetchAttributes( attributeNameList, superObjectClass, must );
        }
    }


    private String getIdString( AbstractSchemaObject schemaObject )
    {
        StringBuilder sb = new StringBuilder();
        sb.append( '[' );
        if ( schemaObject.getNames() != null )
        {
            for ( String name : schemaObject.getNames() )
            {
                sb.append( name );
                sb.append( ',' );
            }
        }
        sb.append( schemaObject.getOid() );
        sb.append( ']' );
        return sb.toString();
    }


    /**
     * {@inheritDoc}
     */
    public void init( IWorkbench workbench, IStructuredSelection selection )
    {
    }
}
