/*
 *  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.aciitemeditor.valueeditors;


import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.subtree.BaseSubtreeSpecification;
import org.apache.directory.api.ldap.model.subtree.SubtreeSpecification;
import org.apache.directory.api.ldap.model.subtree.SubtreeSpecificationParser;
import org.apache.directory.studio.aciitemeditor.Activator;
import org.apache.directory.studio.common.ui.widgets.BaseWidgetUtils;
import org.apache.directory.studio.common.ui.widgets.WidgetModifyEvent;
import org.apache.directory.studio.common.ui.widgets.WidgetModifyListener;
import org.apache.directory.studio.ldapbrowser.common.dialogs.TextDialog;
import org.apache.directory.studio.ldapbrowser.common.widgets.search.EntryWidget;
import org.apache.directory.studio.ldapbrowser.common.widgets.search.FilterWidget;
import org.apache.directory.studio.ldapbrowser.core.model.IBrowserConnection;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Spinner;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Text;


/**
 * This class provides a dialog to enter the Subtree Specification value.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
class SubtreeSpecificationDialog extends Dialog
{
    /** The parser. */
    private final SubtreeSpecificationParser parser = new SubtreeSpecificationParser( null );

    /** The connection */
    private IBrowserConnection connection;

    /** The subentry's Dn */
    private Dn subentryDn;

    /** Flag indicating if the refinement or filter widget should be visible */
    private boolean refinementOrFilterVisible;

    /** Flag indicating if a local name should be used for the base */
    private boolean useLocalName;

    /** The initial SubtreeSpecification */
    private SubtreeSpecification subtreeSpecification;

    /** The Exclusions List */
    private List<String> exclusions;

    /** The returned SubtreeSpecification */
    private String returnValue;

    // UI Fields
    private EntryWidget entryWidget;
    private Spinner minimumSpinner;
    private Spinner maximumSpinner;
    private TableViewer exclusionsTableViewer;
    private Button exclusionsTableEditButton;
    private Button exclusionsTableDeleteButton;
    private Button refinementButton;
    private Text refinementText;
    private Button filterButton;
    private FilterWidget filterWidget;


    /**
     * Creates a new instance of SubtreeSpecificationDialog.
     *
     * @param shell
     *      the shell to use
     * @param connection
     *      the connection to use
     * @param subentryDn
     *      the subentry's Dn
     * @param initialSubtreeSpecification
     *      the initial SubtreeSpecification
     * @param refinementOrFilterVisible
     *      true if the refinement of filter widget should be visible
     * @param useLocalName 
     *      true to use local name for the base
     */
    SubtreeSpecificationDialog( Shell shell, IBrowserConnection connection, Dn subentryDn,
        String initialSubtreeSpecification, boolean refinementOrFilterVisible, boolean useLocalName )
    {
        super( shell );
        this.connection = connection;
        this.subentryDn = subentryDn;
        this.refinementOrFilterVisible = refinementOrFilterVisible;
        this.useLocalName = useLocalName;

        // parse
        try
        {
            subtreeSpecification = parser.parse( initialSubtreeSpecification );
            if ( subtreeSpecification == null )
            {
                subtreeSpecification = new BaseSubtreeSpecification();
            }
        }
        catch ( ParseException pe )
        {
            // TODO
            pe.printStackTrace();
            subtreeSpecification = new BaseSubtreeSpecification();
        }

        exclusions = new ArrayList<String>();
        Set<Dn> chopBeforeExclusions = subtreeSpecification.getChopBeforeExclusions();
        for ( Dn dn : chopBeforeExclusions )
        {
            exclusions.add( "chopBefore: \"" + dn.getNormName() + "\"" ); //$NON-NLS-1$ //$NON-NLS-2$
        }

        Set<Dn> chopAfterExclusions = subtreeSpecification.getChopAfterExclusions();
        for ( Dn dn : chopAfterExclusions )
        {
            exclusions.add( "chopAfter: \"" + dn.getNormName() + "\"" ); //$NON-NLS-1$ //$NON-NLS-2$
        }

        returnValue = null;
    }


    /**
     * {@inheritDoc}
     */
    protected void configureShell( Shell newShell )
    {
        super.configureShell( newShell );
        newShell.setText( Messages.getString( "SubtreeValueEditor.title" ) ); //$NON-NLS-1$
        newShell.setImage( Activator.getDefault().getImage( Messages.getString( "SubtreeValueEditor.icon" ) ) ); //$NON-NLS-1$
    }


    /**
     * {@inheritDoc}
     */
    protected void okPressed()
    {
        // set return value
        //returnValue = buildSubreeSpecification();
        StringBuilder sb = new StringBuilder();
        subtreeSpecification.toString( sb );
        returnValue = sb.toString();

        // save filter and dn history
        if ( refinementOrFilterVisible )
        {
            filterWidget.saveDialogSettings();
        }
        entryWidget.saveDialogSettings();

        super.okPressed();
    }


    /**
     * {@inheritDoc}
     */
    protected Control createDialogArea( Composite parent )
    {
        Composite outer = ( Composite ) super.createDialogArea( parent );
        GridData gd = new GridData( GridData.FILL_BOTH );
        gd.widthHint = convertHorizontalDLUsToPixels( IDialogConstants.MINIMUM_MESSAGE_AREA_WIDTH );
        outer.setLayoutData( gd );

        Composite composite = BaseWidgetUtils.createColumnContainer( outer, 3, 1 );

        BaseWidgetUtils.createLabel( composite, Messages.getString( "SubtreeValueEditor.label.base" ), 1 ); //$NON-NLS-1$

        Dn base = subtreeSpecification.getBase();
        Dn suffix = null;
        
        if ( subentryDn != null ) 
        {
            suffix = subentryDn.getParent();
        }
        
        entryWidget = new EntryWidget( connection, base, suffix, useLocalName );
        entryWidget.createWidget( composite );
        entryWidget.addWidgetModifyListener( new WidgetModifyListener()
        {
            public void widgetModified( WidgetModifyEvent event )
            {
                validate();
            }
        } );

        GridData spinnersGridData = new GridData();
        spinnersGridData.grabExcessHorizontalSpace = true;
        spinnersGridData.verticalAlignment = GridData.CENTER;
        spinnersGridData.horizontalSpan = 2;
        spinnersGridData.horizontalAlignment = GridData.BEGINNING;
        spinnersGridData.widthHint = 3 * 12;

        BaseWidgetUtils.createLabel( composite, Messages.getString( "SubtreeValueEditor.label.minimum" ), 1 ); //$NON-NLS-1$
        minimumSpinner = new Spinner( composite, SWT.BORDER );
        minimumSpinner.setMinimum( 0 );
        minimumSpinner.setMaximum( Integer.MAX_VALUE );
        minimumSpinner.setDigits( 0 );
        minimumSpinner.setIncrement( 1 );
        minimumSpinner.setPageIncrement( 100 );
        minimumSpinner.setSelection( subtreeSpecification.getMinBaseDistance() );
        minimumSpinner.setLayoutData( spinnersGridData );
        minimumSpinner.addModifyListener( new ModifyListener()
        {
            public void modifyText( ModifyEvent event )
            {
                validate();
            }
        } );

        BaseWidgetUtils.createLabel( composite, Messages.getString( "SubtreeValueEditor.label.maximum" ), 1 ); //$NON-NLS-1$
        maximumSpinner = new Spinner( composite, SWT.BORDER );
        maximumSpinner.setMinimum( 0 );
        maximumSpinner.setMaximum( Integer.MAX_VALUE );
        maximumSpinner.setDigits( 0 );
        maximumSpinner.setIncrement( 1 );
        maximumSpinner.setPageIncrement( 100 );
        maximumSpinner.setSelection( subtreeSpecification.getMaxBaseDistance() );
        maximumSpinner.setLayoutData( spinnersGridData );
        maximumSpinner.addModifyListener( new ModifyListener()
        {
            public void modifyText( ModifyEvent event )
            {
                validate();
            }
        } );

        createExclusionsTable( composite );

        if ( refinementOrFilterVisible )
        {
            BaseWidgetUtils.createSpacer( composite, 3 );
            createRefinementOrFilterWidgets( composite );
        }

        applyDialogFont( outer );

        validate();

        return outer;
    }

    /**
     * Creates the Exclusions Table.
     *
     * @param composite
     *      the composite
     */
    private void createExclusionsTable( Composite composite )
    {
        GridData tableGridData = new GridData();
        tableGridData.grabExcessHorizontalSpace = true;
        tableGridData.verticalAlignment = GridData.FILL;
        tableGridData.horizontalAlignment = GridData.FILL;
        tableGridData.heightHint = 100;

        BaseWidgetUtils.createLabel( composite, Messages.getString( "SubtreeValueEditor.label.exclusions" ), 1 ); //$NON-NLS-1$
        Table exclusionsTable = new Table( composite, SWT.BORDER );
        exclusionsTable.setHeaderVisible( false );
        exclusionsTable.setLayoutData( tableGridData );
        exclusionsTable.setLinesVisible( false );
        exclusionsTableViewer = new TableViewer( exclusionsTable );
        exclusionsTableViewer.setContentProvider( new ArrayContentProvider() );
        exclusionsTableViewer.setLabelProvider( new LabelProvider() );
        exclusionsTableViewer.setInput( exclusions );
        exclusionsTableViewer.addSelectionChangedListener( new ISelectionChangedListener()
        {
            public void selectionChanged( SelectionChangedEvent event )
            {
                valueSelectedExclusionsTable();
            }
        } );
        exclusionsTableViewer.addDoubleClickListener( new IDoubleClickListener()
        {
            public void doubleClick( DoubleClickEvent event )
            {
                editValueExclusionsTable();
            }
        } );

        GridLayout gridLayout = new GridLayout();
        gridLayout.marginWidth = 0;
        gridLayout.marginHeight = 0;
        GridData gridData = new GridData();
        gridData.horizontalAlignment = GridData.CENTER;
        gridData.grabExcessHorizontalSpace = false;
        gridData.grabExcessVerticalSpace = false;
        gridData.verticalAlignment = GridData.FILL;

        Composite buttonComposite = new Composite( composite, SWT.NONE );
        buttonComposite.setLayoutData( gridData );
        buttonComposite.setLayout( gridLayout );

        GridData buttonGridData = new GridData();
        buttonGridData.horizontalAlignment = GridData.FILL;
        buttonGridData.grabExcessHorizontalSpace = false;
        buttonGridData.verticalAlignment = GridData.BEGINNING;
        buttonGridData.widthHint = Activator.getButtonWidth( buttonComposite );

        Button exclusionsTableAddButton = new Button( buttonComposite, SWT.PUSH );
        exclusionsTableAddButton.setText( Messages.getString( "SubtreeValueEditor.button.add" ) ); //$NON-NLS-1$
        exclusionsTableAddButton.setLayoutData( buttonGridData );
        exclusionsTableAddButton.addSelectionListener( new SelectionAdapter()
        {
            public void widgetSelected( SelectionEvent e )
            {
                addValueExclusionsTable();
            }
        } );

        exclusionsTableEditButton = new Button( buttonComposite, SWT.PUSH );
        exclusionsTableEditButton.setText( Messages.getString( "SubtreeValueEditor.button.edit" ) ); //$NON-NLS-1$
        exclusionsTableEditButton.setLayoutData( buttonGridData );
        exclusionsTableEditButton.addSelectionListener( new SelectionAdapter()
        {
            public void widgetSelected( SelectionEvent e )
            {
                editValueExclusionsTable();
            }
        } );
        exclusionsTableEditButton.setEnabled( false );

        exclusionsTableDeleteButton = new Button( buttonComposite, SWT.PUSH );
        exclusionsTableDeleteButton.setText( Messages.getString( "SubtreeValueEditor.button.delete" ) ); //$NON-NLS-1$
        exclusionsTableDeleteButton.setLayoutData( buttonGridData );
        exclusionsTableDeleteButton.addSelectionListener( new SelectionAdapter()
        {
            public void widgetSelected( SelectionEvent e )
            {
                deleteValueExclusionsTable();
            }
        } );
        exclusionsTableDeleteButton.setEnabled( false );
    }


    /**
     * Creates the refinement or filter widgets
     *
     * @param composite
     *      the composite
     */
    private void createRefinementOrFilterWidgets( Composite parent )
    {
        // Messages.getString( "SubtreeValueEditor.label.exclusions" )
        BaseWidgetUtils.createLabel( parent, Messages
            .getString( "SubtreeValueEditor.SubtreeValueEditor.label.refinementOrFilter" ), 1 ); //$NON-NLS-1$

        Composite composite = BaseWidgetUtils.createColumnContainer( parent, 2, 2 );

        // refinement redio button
        refinementButton = BaseWidgetUtils.createRadiobutton( composite, Messages
            .getString( "SubtreeValueEditor.SubtreeValueEditor.label.refinement" ), 2 ); //$NON-NLS-1$

        // refinement text
        refinementText = new Text( composite, SWT.MULTI | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL );
        GridData gd = new GridData( GridData.FILL_BOTH );
        gd.horizontalSpan = 2;
        gd.widthHint = convertHorizontalDLUsToPixels( IDialogConstants.MINIMUM_MESSAGE_AREA_WIDTH );
        gd.heightHint = convertHorizontalDLUsToPixels( IDialogConstants.MINIMUM_MESSAGE_AREA_WIDTH / 6 );
        refinementText.setLayoutData( gd );
        try
        {
            StringBuilder refinementBuffer = new StringBuilder();
            if ( subtreeSpecification.getRefinement() != null )
            {
                subtreeSpecification.getRefinement().printRefinementToBuffer( refinementBuffer );
            }
            refinementText.setText( refinementBuffer.toString().trim() );
            refinementText.setEnabled( true );
            refinementButton.setSelection( true );
        }
        catch ( UnsupportedOperationException e )
        {
            // thrown if the ExprNode doesn't represent a valid refinement
            refinementText.setText( "" ); //$NON-NLS-1$
            refinementText.setEnabled( false );
            refinementButton.setSelection( false );
        }

        // filter radio button
        filterButton = BaseWidgetUtils.createRadiobutton( composite, Messages
            .getString( "SubtreeValueEditor.SubtreeValueEditor.label.filter" ), 2 ); //$NON-NLS-1$

        // filter widget
        String filter = ""; //$NON-NLS-1$
        if ( subtreeSpecification.getRefinement() != null )
        {
            filter = subtreeSpecification.getRefinement().toString();
        }
        filterWidget = new FilterWidget( filter );
        filterWidget.createWidget( composite );
        filterWidget.setBrowserConnection( connection );
        filterButton.setSelection( !refinementButton.getSelection() );
        filterWidget.setEnabled( !refinementButton.getSelection() );

        // add listeners
        refinementButton.addSelectionListener( new SelectionAdapter()
        {
            public void widgetSelected( SelectionEvent arg0 )
            {
                refinementText.setEnabled( true );
                //filterButton.setSelection( false );
                filterWidget.setEnabled( false );
                validate();
            }
        } );
        refinementText.addModifyListener( new ModifyListener()
        {
            public void modifyText( ModifyEvent event )
            {
                validate();
            }
        } );
        filterButton.addSelectionListener( new SelectionAdapter()
        {
            public void widgetSelected( SelectionEvent arg0 )
            {
                //refinementButton.setSelection( false );
                refinementText.setEnabled( false );
                filterWidget.setEnabled( true );
                validate();
            }
        } );
        filterWidget.addWidgetModifyListener( new WidgetModifyListener()
        {
            public void widgetModified( WidgetModifyEvent event )
            {
                validate();
            }
        } );
    }


    /**
     * Validates if the composed subtree specification is valid.
     */
    private void validate()
    {
        boolean valid = true;

        Dn base = entryWidget.getDn();
        valid &= base != null;

        String ss = buildSubreeSpecification();

        try
        {
            subtreeSpecification = parser.parse( ss );
            valid &= true;
        }
        catch ( ParseException pe )
        {
            subtreeSpecification = null;
            valid &= false;
        }

        if ( refinementOrFilterVisible && filterButton.getSelection() )
        {
            valid &= filterWidget.getFilter() != null;
        }

        if ( getButton( IDialogConstants.OK_ID ) != null )
        {
            getButton( IDialogConstants.OK_ID ).setEnabled( valid );
        }
    }


    private String buildSubreeSpecification()
    {
        // build subtree specification tree
        StringBuilder sb = new StringBuilder();
        sb.append( "{" ); //$NON-NLS-1$

        // Adding base
        Dn base = entryWidget.getDn();
        
        if ( ( base != null ) && !SubtreeValueEditor.EMPTY.equals( base.toString() ) )
        {
            sb.append( " base \"" );
            sb.append( base.toString() );
            sb.append( "\"," ); //$NON-NLS-1$ //$NON-NLS-2$
        }

        // Adding Minimum
        int minimum = minimumSpinner.getSelection();
        
        if ( minimum != 0 )
        {
            sb.append( " minimum " ).append( minimum ).append( ',' ); //$NON-NLS-1$ //$NON-NLS-2$
        }

        // Adding Maximum
        int maximum = maximumSpinner.getSelection();
        
        if ( maximum != 0 )
        {
            sb.append( " maximum " ).append( maximum ).append( ',' ); //$NON-NLS-1$ //$NON-NLS-2$
        }

        // Adding Exclusions
        if ( !exclusions.isEmpty() )
        {
            sb.append( " specificExclusions {" ); //$NON-NLS-1$

            boolean isFirst = true;
            
            for ( String exclusion : exclusions )
            {
                if ( isFirst )
                {
                    isFirst = false;
                }
                else
                {
                    sb.append( ',' );
                }
                
                sb.append( ' ' ).append( exclusion ); //$NON-NLS-1$
            }

            sb.append( " }," ); //$NON-NLS-1$
        }

        // Add Refinement or Filter
        String refinementOrFilter = ""; //$NON-NLS-1$
        if ( refinementOrFilterVisible )
        {
            if ( refinementButton.getSelection() )
            {
                refinementOrFilter = refinementText.getText();
            }
            else
            {
                refinementOrFilter = filterWidget.getFilter();
            }
        }
        else
        {
            refinementOrFilter = ""; //$NON-NLS-1$
        }
        if ( refinementOrFilter != null && !SubtreeValueEditor.EMPTY.equals( refinementOrFilter ) )
        {
            sb.append( " specificationFilter " + refinementOrFilter + "," ); //$NON-NLS-1$ //$NON-NLS-2$
        }

        // Removing the last ','
        if ( sb.charAt( sb.length() - 1 ) == ',' )
        {
            sb.deleteCharAt( sb.length() - 1 );
        }

        sb.append( " }" ); //$NON-NLS-1$

        return sb.toString();
    }


    /**
     * Called when value is selected in Exclusions table viewer.
     * Updates the enabled/disabled state of the buttons.
     */
    private void valueSelectedExclusionsTable()
    {
        String value = getSelectedValueExclusionsTable();

        if ( value == null )
        {
            exclusionsTableEditButton.setEnabled( false );
            exclusionsTableDeleteButton.setEnabled( false );
        }
        else
        {
            exclusionsTableEditButton.setEnabled( true );
            exclusionsTableDeleteButton.setEnabled( true );
        }
    }


    /**
     * Retuns the current selection in the Exclusions table viewer.
     *
     * @return
     *      the value that is selected in the Exclusions table viewer, or null.
     */
    private String getSelectedValueExclusionsTable()
    {
        String value = null;

        IStructuredSelection selection = ( IStructuredSelection ) exclusionsTableViewer.getSelection();
        if ( !selection.isEmpty() )
        {
            Object element = selection.getFirstElement();
            if ( element instanceof String )
            {
                value = ( String ) element;
            }
        }

        return value;
    }


    /**
     * Opens the editor and adds the new Exclusion value to the list.
     */
    private void addValueExclusionsTable()
    {
        Dn chopBase = subtreeSpecification.getBase();

        if ( useLocalName && ( subentryDn != null ) )
        {
            Dn suffix = subentryDn.getParent();

            if ( !Dn.isNullOrEmpty( suffix ) )
            {
                try
                {
                    chopBase = chopBase.add( suffix );
                }
                catch ( LdapInvalidDnException lide )
                {
                    // Do nothing 
                }
            }
        }

        ExclusionDialog dialog = new ExclusionDialog( getShell(), connection, chopBase, "" ); //$NON-NLS-1$

        if ( dialog.open() == TextDialog.OK && !SubtreeValueEditor.EMPTY.equals( dialog.getType() )
            && !SubtreeValueEditor.EMPTY.equals( dialog.getDN() ) )
        {
            String newValue = dialog.getType() + ": \"" + dialog.getDN() + "\""; //$NON-NLS-1$ //$NON-NLS-2$
            exclusions.add( newValue );
            exclusionsTableViewer.refresh();
            validate();
        }
    }


    /**
     * Opens the editor with the currently selected Exclusion
     * value and puts the modified value into the list.
     */
    private void editValueExclusionsTable()
    {
        String oldValue = getSelectedValueExclusionsTable();
        if ( oldValue != null )
        {
            Dn chopBase = subtreeSpecification.getBase();

            if ( useLocalName && ( subentryDn != null ) )
            {
                Dn suffix = subentryDn.getParent();

                if ( !Dn.isNullOrEmpty( suffix ) )
                {
                    try
                    {
                        chopBase = chopBase.add( suffix );
                    }
                    catch ( LdapInvalidDnException lide )
                    {
                        // Do nothing 
                    }

                }
            }

            ExclusionDialog dialog = new ExclusionDialog( getShell(), connection, chopBase, oldValue );

            if ( dialog.open() == TextDialog.OK && !SubtreeValueEditor.EMPTY.equals( dialog.getType() )
                && !SubtreeValueEditor.EMPTY.equals( dialog.getDN() ) )
            {
                String newValue = dialog.getType() + ": \"" + dialog.getDN() + "\""; //$NON-NLS-1$ //$NON-NLS-2$
                exclusions.remove( oldValue );
                exclusions.add( newValue );
                exclusionsTableViewer.refresh();
                validate();
            }
        }
    }


    /**
     * Deletes the currently selected Exclusion value from list.
     */
    private void deleteValueExclusionsTable()
    {
        String value = getSelectedValueExclusionsTable();
        if ( value != null )
        {
            exclusions.remove( value );
            exclusionsTableViewer.refresh();
            validate();
        }
    }


    /**
     * Gets the subtree specification value or null if canceled.
     *
     * @return the subtree specification value or null if canceled
     */
    public String getSubtreeSpecificationValue()
    {
        return returnValue;
    }
}