/**
 * Copyright (c) 2010-2012 Kenn Hussey and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors: 
 *   Kenn Hussey - Initial API and implementation
 */
package org.eclipse.emf.oda.ecore.ui.impl;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;

import org.eclipse.datatools.connectivity.oda.IConnection;
import org.eclipse.datatools.connectivity.oda.IDriver;
import org.eclipse.datatools.connectivity.oda.IParameterMetaData;
import org.eclipse.datatools.connectivity.oda.IQuery;
import org.eclipse.datatools.connectivity.oda.IResultSetMetaData;
import org.eclipse.datatools.connectivity.oda.OdaException;
import org.eclipse.datatools.connectivity.oda.design.DataElementAttributes;
import org.eclipse.datatools.connectivity.oda.design.DataSetDesign;
import org.eclipse.datatools.connectivity.oda.design.DataSetParameters;
import org.eclipse.datatools.connectivity.oda.design.DesignFactory;
import org.eclipse.datatools.connectivity.oda.design.ParameterDefinition;
import org.eclipse.datatools.connectivity.oda.design.ParameterMode;
import org.eclipse.datatools.connectivity.oda.design.ResultSetColumns;
import org.eclipse.datatools.connectivity.oda.design.ResultSetDefinition;
import org.eclipse.datatools.connectivity.oda.design.ui.designsession.DesignSessionUtil;
import org.eclipse.datatools.connectivity.oda.design.ui.wizards.DataSetWizardPage;
import org.eclipse.datatools.connectivity.oda.design.util.DesignUtil;
import org.eclipse.datatools.connectivity.oda.spec.QuerySpecification;
import org.eclipse.datatools.connectivity.oda.spec.util.QuerySpecificationHelper;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.UniqueEList;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EDataType;
import org.eclipse.emf.ecore.EEnum;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EcorePackage;
import org.eclipse.emf.ecore.provider.EcoreEditPlugin;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.QueryDelegate;
import org.eclipse.emf.edit.ui.provider.ExtendedImageRegistry;
import org.eclipse.emf.edit.ui.util.QueryDelegateTextViewer;
import org.eclipse.emf.oda.ecore.impl.Connection;
import org.eclipse.emf.oda.ecore.impl.Driver;
import org.eclipse.emf.oda.ecore.impl.ParameterMetaData;
import org.eclipse.emf.oda.ecore.impl.Query;
import org.eclipse.emf.oda.ecore.ui.ODAEcoreUIPlugin;
import org.eclipse.emf.oda.ecore.util.StringUtil;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.ITextListener;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.TextEvent;
import org.eclipse.jface.text.TextViewer;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableLayout;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.dialogs.ElementListSelectionDialog;


/**
 * Implementation of an EMF ODA data set designer page for a user to create or edit a data set
 * design instance.
 */
public class EcoreDataSetWizardPage extends DataSetWizardPage
{
  protected static String DEFAULT_MESSAGE = ODAEcoreUIPlugin.INSTANCE.getString("_UI_DefaultDataSet_message"); //$NON-NLS-1$

  protected Combo queryDelegateCombo = null;

  protected Map<String, ITextViewer> queryTextViewers = new HashMap<String, ITextViewer>();
  protected Composite queryTextViewerComposite = null;
  protected StackLayout queryTextViewerStackLayout = null;

  protected TableViewer variablesViewer = null;
  protected List<Variable> variables = new UniqueEList<Variable>();

  protected Text contextTypeField = null;
  protected EClassifier contextType = null;

  protected Text resultTypeField = null;
  protected EClassifier resultType = null;

  public EcoreDataSetWizardPage(String pageName)
  {
    super(pageName);

    setTitle(pageName);
    setMessage(DEFAULT_MESSAGE);
    setImageDescriptor(ExtendedImageRegistry.INSTANCE.getImageDescriptor(ODAEcoreUIPlugin.INSTANCE.getImage("full/wizban/NewEcore"))); //$NON-NLS-1$
  }

  public EcoreDataSetWizardPage(String pageName, String title, ImageDescriptor titleImage)
  {
    super(pageName, title, titleImage);
    setMessage(DEFAULT_MESSAGE);
  }

  @Override
  public void createPageCustomControl(Composite parent)
  {
    setControl(createPageControl(parent));
    initializeControl();
  }

  /**
   * Creates custom page control for user-defined queries.
   */
  protected Control createPageControl(Composite parent)
  {
    Composite composite = new Composite(parent, SWT.NONE);
    composite.setLayout(new GridLayout(3, false));

    GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(composite);

    createQueryDelegateControl(composite);
    createContextTypeControl(composite);
    createVariablesControl(composite);
    createQueryTextControl(composite);
    createResultTypeControl(composite);

    setPageComplete(false);

    return composite;
  }

  /**
   * Creates control for query delegate property.
   */
  protected void createQueryDelegateControl(Composite parent)
  {
    Label queryDelegateLabel = new Label(parent, SWT.NONE);
    queryDelegateLabel.setText(ODAEcoreUIPlugin.INSTANCE.getString("_UI_QueryDelegate_label")); //$NON-NLS-1$

    queryDelegateCombo = new Combo(parent, SWT.BORDER | SWT.READ_ONLY);
    queryDelegateCombo.addModifyListener(new ModifyListener()
      {
        public void modifyText(ModifyEvent me)
        {
          String queryDelegate = getQueryDelegate();
          QueryDelegateTextViewer.Factory factory = (QueryDelegateTextViewer.Factory)QueryDelegateTextViewer.Factory.Registry.INSTANCE.get(queryDelegate);

          ITextViewer queryTextViewer = queryTextViewers.get(queryDelegate);

          if (queryTextViewer == null)
          {
            queryTextViewer = factory != null
              ? factory.createTextViewer(queryTextViewerComposite, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL) : new TextViewer(
                queryTextViewerComposite,
                SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);

            if (queryTextViewer.getDocument() == null)
            {
              queryTextViewer.setDocument(new Document());
            }

            queryTextViewer.addTextListener(new ITextListener()
              {
                public void textChanged(TextEvent te)
                {
                  validateData();
                }
              });

            queryTextViewers.put(queryDelegate, queryTextViewer);
          }

          queryTextViewerStackLayout.topControl = queryTextViewer.getTextWidget();
          queryTextViewerComposite.layout();

          validateData();
        }
      });

    for (String uri : QueryDelegate.Factory.Registry.INSTANCE.keySet())
    {
      queryDelegateCombo.add(uri);
    }

    GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, false).span(2, 1).applyTo(queryDelegateCombo);
  }

  /**
   * A provider of formatted labels for types.
   */
  protected class TypeLabelProvider extends LabelProvider
  {
    @Override
    public String getText(Object element)
    {
      return StringUtil.getTypeText((EClassifier)element);
    }

    @Override
    public Image getImage(Object element)
    {
      String typeName = element instanceof EClass ? "EClass" : (element instanceof EEnum ? "EEnum" : "EDataType"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
      return ExtendedImageRegistry.getInstance().getImage(EcoreEditPlugin.INSTANCE.getImage("full/obj16/" + typeName)); //$NON-NLS-1$
    }
  }

  /**
   * Opens a dialog prompting the user to select a context type.
   */
  protected void promptForContextTypeSelection()
  {
    ElementListSelectionDialog dialog = new ElementListSelectionDialog(getShell(), new TypeLabelProvider());

    dialog.setMessage(ODAEcoreUIPlugin.INSTANCE.getString("_UI_SelectContextType")); //$NON-NLS-1$          
    dialog.setFilter("*"); //$NON-NLS-1$
    dialog.setTitle(ODAEcoreUIPlugin.INSTANCE.getString("_UI_ContextTypeSelection_title")); //$NON-NLS-1$
    dialog.setElements(getClassChoices());

    if (contextType != null)
    {
      dialog.setInitialElementSelections(Collections.singletonList(contextType));
    }

    if (dialog.open() == Window.OK)
    {
      setContextType((EClass)dialog.getFirstResult());
      validateData();
    }
  }

  /**
   * Creates control for context type property.
   */
  protected void createContextTypeControl(Composite parent)
  {
    Label contextTypeLabel = new Label(parent, SWT.NONE);
    contextTypeLabel.setText(ODAEcoreUIPlugin.INSTANCE.getString("_UI_ContextType_label")); //$NON-NLS-1$

    contextTypeField = new Text(parent, SWT.BORDER | SWT.READ_ONLY);
    contextTypeField.addMouseListener(new MouseAdapter()
      {
        @Override
        public void mouseDoubleClick(MouseEvent me)
        {
          promptForContextTypeSelection();
        }
      });

    GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, false).applyTo(contextTypeField);

    Button contextTypeButton = new Button(parent, SWT.PUSH);
    contextTypeButton.setText(ODAEcoreUIPlugin.INSTANCE.getString("_UI_Select_label")); //$NON-NLS-1$
    contextTypeButton.addSelectionListener(new SelectionAdapter()
      {
        @Override
        public void widgetSelected(SelectionEvent se)
        {
          promptForContextTypeSelection();
        }
      });
  }

  /**
   * Representation of an expression variable.
   */
  protected static class Variable
  {
    protected final String name;
    protected final EClassifier type;

    protected Variable(String name, EClassifier type)
    {
      super();

      this.name = name;
      this.type = type;
    }

    /**
     * Returns the name of this expression variable.
     * @return the name
     */
    protected String getName()
    {
      return name;
    }

    /**
     * Returns the type of this expression variable.
     * @return the type
     */
    protected EClassifier getType()
    {
      return type;
    }
  }

  /**
   * A dialog which prompts the user to specify a name and select a type for an
   * expression variable.
   */
  protected class VariableSpecificationDialog extends ElementListSelectionDialog
  {
    protected Text nameField = null;
    protected String initialName = null;

    protected VariableSpecificationDialog(Shell shell)
    {
      super(shell, new TypeLabelProvider());

      setMessage(ODAEcoreUIPlugin.INSTANCE.getString("_UI_SpecifyNameSelectType")); //$NON-NLS-1$
      setFilter("*"); //$NON-NLS-1$
      setElements(getClassifierChoices());
    }

    @Override
    protected void computeResult()
    {
      Object[] selectedElements = getSelectedElements();
      setResult(Collections.singletonList(new Variable(nameField.getText(), (EClassifier)selectedElements[0])));
    }

    @Override
    protected Text createFilterText(Composite parent)
    {
      nameField = new Text(parent, SWT.BORDER);
      nameField.setText(initialName == null ? ODAEcoreUIPlugin.INSTANCE.getString("_UI_VariableName_label") : initialName); //$NON-NLS-1$

      GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, false).applyTo(nameField);

      return super.createFilterText(parent);
    }

    protected void setInitialName(String initialName)
    {
      this.initialName = initialName;
    }
  }

  /**
   * Creates control for expression variables property.
   */
  protected void createVariablesControl(Composite parent)
  {
    Label variablesLabel = new Label(parent, SWT.NONE);
    variablesLabel.setText(ODAEcoreUIPlugin.INSTANCE.getString("_UI_Variables_label")); //$NON-NLS-1$

    final Table variablesTable = new Table(parent, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI);

    TableLayout tableLayout = new TableLayout();
    tableLayout.addColumnData(new ColumnWeightData(2));
    tableLayout.addColumnData(new ColumnWeightData(3));

    variablesTable.setLayout(tableLayout);

    GridDataFactory.fillDefaults().grab(true, true).applyTo(variablesTable);

    variablesTable.setHeaderVisible(true);
    variablesTable.setLinesVisible(true);

    TableColumn nameColumn = new TableColumn(variablesTable, SWT.NONE);
    nameColumn.setText(ODAEcoreUIPlugin.INSTANCE.getString("_UI_VariableName_label")); //$NON-NLS-1$

    TableColumn typeColumn = new TableColumn(variablesTable, SWT.NONE);
    typeColumn.setText(ODAEcoreUIPlugin.INSTANCE.getString("_UI_VariableType_label")); //$NON-NLS-1$

    variablesViewer = new TableViewer(variablesTable);
    variablesViewer.setContentProvider(new IStructuredContentProvider()
      {
        public void inputChanged(Viewer viewer, Object oldInput, Object newInput)
        {
          ITextViewer queryTextViewer = getQueryTextViewer();

          if (queryTextViewer instanceof QueryDelegateTextViewer)
          {
            ((QueryDelegateTextViewer)queryTextViewer).setParameters(convertVariablesToMap(getVariables()));
          }
        }

        public void dispose()
        {
          // Ignore
        }

        @SuppressWarnings("unchecked")
        public Object[] getElements(Object inputElement)
        {
          return ((List<Variable>)inputElement).toArray();
        }
      });
    variablesViewer.setLabelProvider(new ITableLabelProvider()
      {
        public void removeListener(ILabelProviderListener listener)
        {
          // Ignore
        }

        public boolean isLabelProperty(Object element, String property)
        {
          return false;
        }

        public void dispose()
        {
          // Ignore
        }

        public void addListener(ILabelProviderListener listener)
        {
          // Ignore
        }

        public String getColumnText(Object element, int columnIndex)
        {
          Variable variable = (Variable)element;
          return columnIndex == 0 ? variable.getName() : StringUtil.getTypeText(variable.getType());
        }

        public Image getColumnImage(Object element, int columnIndex)
        {
          return null;
        }
      });

    Composite variablesButtons = new Composite(parent, SWT.NONE);
    GridLayout gridLayout = new GridLayout(1, true);
    gridLayout.marginWidth = 0;
    variablesButtons.setLayout(gridLayout);

    GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).applyTo(variablesButtons);

    Button newButton = new Button(variablesButtons, SWT.PUSH);
    newButton.setText(ODAEcoreUIPlugin.INSTANCE.getString("_UI_VariableNew_label")); //$NON-NLS-1$
    newButton.addSelectionListener(new SelectionAdapter()
      {
        @Override
        public void widgetSelected(SelectionEvent se)
        {
          VariableSpecificationDialog dialog = new VariableSpecificationDialog(getShell());
          dialog.setTitle(ODAEcoreUIPlugin.INSTANCE.getString("_UI_VariableNew_title")); //$NON-NLS-1$

          if (dialog.open() == Window.OK)
          {
            List<Variable> variables = getVariables();
            variables.add((Variable)dialog.getFirstResult());

            variablesViewer.setInput(variables);

            validateData();
          }
        }
      });

    GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).applyTo(newButton);

    final Button editButton = new Button(variablesButtons, SWT.PUSH);
    editButton.setText(ODAEcoreUIPlugin.INSTANCE.getString("_UI_VariableEdit_label")); //$NON-NLS-1$
    editButton.addSelectionListener(new SelectionAdapter()
      {
        @Override
        public void widgetSelected(SelectionEvent se)
        {
          VariableSpecificationDialog dialog = new VariableSpecificationDialog(getShell());
          dialog.setTitle(ODAEcoreUIPlugin.INSTANCE.getString("_UI_VariableEdit_title")); //$NON-NLS-1$

          Variable variable = (Variable)((IStructuredSelection)variablesViewer.getSelection()).getFirstElement();
          dialog.setInitialName(variable.getName());
          dialog.setInitialElementSelections(Collections.singletonList(variable.getType()));

          if (dialog.open() == Window.OK)
          {
            List<Variable> variables = getVariables();
            variables.set(variables.indexOf(variable), (Variable)dialog.getFirstResult());

            variablesViewer.setInput(variables);

            validateData();
          }
        }
      });
    editButton.setEnabled(false);

    GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).applyTo(editButton);

    final Button removeButton = new Button(variablesButtons, SWT.PUSH);
    removeButton.setText(ODAEcoreUIPlugin.INSTANCE.getString("_UI_VariableRemove_label")); //$NON-NLS-1$
    removeButton.addSelectionListener(new SelectionAdapter()
      {
        @Override
        public void widgetSelected(SelectionEvent se)
        {
          List<Variable> variables = getVariables();
          ISelection selection = variablesViewer.getSelection();

          for (Iterator< ? > selections = ((IStructuredSelection)selection).iterator(); selections.hasNext();)
          {
            variables.remove(selections.next());
          }

          variablesViewer.setInput(variables);

          validateData();
        }
      });
    removeButton.setEnabled(false);

    GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).applyTo(removeButton);

    variablesViewer.addSelectionChangedListener(new ISelectionChangedListener()
      {
        public void selectionChanged(SelectionChangedEvent sce)
        {
          IStructuredSelection selection = (IStructuredSelection)sce.getSelection();

          editButton.setEnabled(selection.size() == 1);
          removeButton.setEnabled(selection.size() > 0);
        }
      });
  }

  /**
   * Creates control for query text.
   */
  protected void createQueryTextControl(Composite parent)
  {
    Label queryTextLabel = new Label(parent, SWT.NONE);
    queryTextLabel.setText(ODAEcoreUIPlugin.INSTANCE.getString("_UI_QueryText_label")); //$NON-NLS-1$

    (queryTextViewerComposite = new Composite(parent, SWT.NONE)).setLayout(queryTextViewerStackLayout = new StackLayout());

    GridDataFactory.fillDefaults().grab(true, true).span(2, 1).applyTo(queryTextViewerComposite);
  }

  /**
   * Opens a dialog prompting the user to select a result type.
   */
  protected void promptForResultTypeSelection()
  {
    ElementListSelectionDialog dialog = new ElementListSelectionDialog(getShell(), new TypeLabelProvider());

    dialog.setMessage(ODAEcoreUIPlugin.INSTANCE.getString("_UI_SelectResultType")); //$NON-NLS-1$
    dialog.setFilter("*"); //$NON-NLS-1$
    dialog.setTitle(ODAEcoreUIPlugin.INSTANCE.getString("_UI_ResultTypeSelection_title")); //$NON-NLS-1$
    dialog.setElements(getClassifierChoices());

    if (resultType != null)
    {
      dialog.setInitialElementSelections(Collections.singletonList(resultType));
    }

    if (dialog.open() == Window.OK)
    {
      setResultType((EClassifier)dialog.getFirstResult());
      validateData();
    }
  }

  /**
   * Creates control for result type property.
   */
  protected void createResultTypeControl(Composite parent)
  {
    Label resultTypeLabel = new Label(parent, SWT.NONE);
    resultTypeLabel.setText(ODAEcoreUIPlugin.INSTANCE.getString("_UI_ResultType_label")); //$NON-NLS-1$

    resultTypeField = new Text(parent, SWT.BORDER | SWT.READ_ONLY);
    resultTypeField.addMouseListener(new MouseAdapter()
      {
        @Override
        public void mouseDoubleClick(MouseEvent me)
        {
          promptForResultTypeSelection();
        }
      });

    GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, false).applyTo(resultTypeField);

    Button resultTypeButton = new Button(parent, SWT.PUSH);
    resultTypeButton.setText(ODAEcoreUIPlugin.INSTANCE.getString("_UI_Select_label")); //$NON-NLS-1$
    resultTypeButton.addSelectionListener(new SelectionAdapter()
      {
        @Override
        public void widgetSelected(SelectionEvent se)
        {
          promptForResultTypeSelection();
        }
      });
  }

  /**
   * Initializes the page control with the last edited data set design.
   */
  protected void initializeControl()
  {
    // restore the last saved data set design
    DataSetDesign dataSetDesign = getEditingDesign();

    if (dataSetDesign == null)
    {
      return; // nothing to initialize
    }

    Properties properties = DesignUtil.convertDesignProperties(dataSetDesign.getPublicProperties());

    String queryDelegate = properties.getProperty(Query.DELEGATE_PROPERTY_NAME);

    if (!StringUtil.isEmpty(queryDelegate))
    {
      // initialize query delegate
      queryDelegateCombo.setText(queryDelegate);
    }
    else if (queryDelegateCombo.getItemCount() > 0)
    {
      queryDelegateCombo.setText(queryDelegateCombo.getItem(0));
    }

    String queryText = dataSetDesign.getQueryText();

    if (!StringUtil.isEmpty(queryText))
    {
      // initialize query text
      getQueryTextViewer().getDocument().set(queryText);
    }

    String context = properties.getProperty(Query.CONTEXT_PROPERTY_NAME);

    if (!StringUtil.isEmpty(context))
    {
      // initialize context type
      try
      {
        setContextType(getTypeFromString(context));
      }
      catch (Exception e)
      {
        // ignore
      }
    }

    String string = properties.getProperty(Query.VARIABLES_PROPERTY_NAME);

    if (!StringUtil.isEmpty(string))
    {
      // initialize expression variables
      try
      {
        List<Variable> variables = getVariables();
        variables.addAll(getVariablesFromString(string));

        variablesViewer.setInput(variables);
      }
      catch (Exception e)
      {
        // ignore
      }
    }

    String type = properties.getProperty(Query.TYPE_PROPERTY_NAME);

    if (!StringUtil.isEmpty(type))
    {
      // initialize result type
      try
      {
        setResultType(getTypeFromString(type));
      }
      catch (Exception e)
      {
        // ignore
      }
    }

    validateData();

    setMessage(DEFAULT_MESSAGE);
  }

  protected Object[] classChoices = null;

  /**
   * Returns an array of classes applicable for the data source.
   * @return the applicable classes
   */
  protected Object[] getClassChoices()
  {
    if (classChoices == null)
    {
      EList<Object> choices = new UniqueEList.FastCompare<Object>();
      choices.addAll(EcoreUtil.getObjectsByType(Arrays.asList(getClassifierChoices()), EcorePackage.Literals.ECLASS));
      classChoices = choices.toArray();
    }

    return classChoices;
  }

  protected Object[] classifierChoices = null;

  /**
   * Returns an array of classes and data types applicable for the data source.
   * @return the applicable classes and data types
   */
  protected Object[] getClassifierChoices()
  {
    if (classifierChoices == null)
    {
      EList<Object> choices = new UniqueEList.FastCompare<Object>();

      Connection connection = null;

      try
      {
        IDriver driver = new Driver();
        connection = (Connection)driver.getConnection(null);

        java.util.Properties properties = DesignSessionUtil.getEffectiveDataSourceProperties(getEditingDesign().getDataSourceDesign());
        connection.open(properties);

        for (TreeIterator<Object> allContents = EcoreUtil.getAllContents(connection.getResourceSet(), true); allContents.hasNext();)
        {
          Object next = allContents.next();

          if (next instanceof EObject)
          {
            EClass eClass = ((EObject)next).eClass();

            if (!choices.contains(eClass))
            {
              choices.add(eClass);

              choices.addAll(eClass.getEAllSuperTypes());

              for (EAttribute eAttribute : eClass.getEAllAttributes())
              {
                EDataType eType = eAttribute.getEAttributeType();

                if (eType != null)
                {
                  choices.add(eType);
                }
              }
            }
          }
        }
      }
      catch (OdaException oe)
      {
        oe.printStackTrace();
      }
      finally
      {
        closeConnection(connection);
      }

      classifierChoices = choices.toArray();
    }

    return classifierChoices;
  }

  /**
   * Converts the specified type to its string representation.
   * @param type the type
   * @return the string representation
   */
  protected String convertTypeToString(EClassifier type)
  {
    return EcoreUtil.getURI(type).toString();
  }

  /**
   * Retrieves the type corresponding to the specified string representation.
   * @param string the string representation
   * @return the type
   */
  protected EClassifier getTypeFromString(String string)
  {
    return (EClassifier)new ResourceSetImpl().getEObject(URI.createURI(string), true);
  }

  /**
   * Converts the specified expression variables to a string representation.
   * @param variables the expression variables
   * @return the string representation
   */
  protected String convertVariablesToString(List<Variable> variables)
  {
    StringBuilder sb = new StringBuilder();

    for (Variable variable : variables)
    {
      sb.append(variable.getName());
      sb.append(' ');
      sb.append(convertTypeToString(variable.getType()));
      sb.append(' ');
    }

    return sb.toString().trim();
  }

  /**
   * Converts the specified expression variables to a map representation.
   * @param variables the expression variables
   * @return the map representation
   */
  protected Map<String, EClassifier> convertVariablesToMap(List<Variable> variables)
  {
    Map<String, EClassifier> map = new HashMap<String, EClassifier>();

    for (Variable variable : variables)
    {
      map.put(variable.getName(), variable.getType());
    }

    return map;
  }

  /**
   * Retrieves the expression variables corresponding to the specified string representation.
   * @param string the string representation
   * @return the variables
   */
  protected List<Variable> getVariablesFromString(String string)
  {
    List<Variable> variables = new UniqueEList<Variable>();
    String[] strings = string.split(Pattern.quote(" ")); //$NON-NLS-1$

    for (int i = 0; i < strings.length; i += 2)
    {
      variables.add(new Variable(strings[i], getTypeFromString(strings[i + 1])));
    }

    return variables;
  }

  /**
   * Returns the value of the query delegate property.
   * @return the query delegate
   */
  protected String getQueryDelegate()
  {
    return queryDelegateCombo == null || queryDelegateCombo.isDisposed() ? null : queryDelegateCombo.getText();
  }

  /**
   * Returns the value of the expression variables property.
   * @return the expression variables
   */
  protected List<Variable> getVariables()
  {
    return variables;
  }

  protected ITextViewer getQueryTextViewer()
  {
    return queryTextViewers.get(getQueryDelegate());
  }

  /**
   * Returns the value of the query text.
   * @return the query text
   */
  protected String getQueryText()
  {
    ITextViewer queryTextViewer = getQueryTextViewer();
    return queryTextViewer == null ? null : queryTextViewer.getDocument().get();
  }

  /**
   * Returns the value of the context type property.
   * @return the context type
   */
  protected EClassifier getContextType()
  {
    return contextType;
  }

  /**
   * Sets the value of the context type property.
   * @param contextType the value
   */
  protected void setContextType(EClassifier contextType)
  {
    this.contextType = contextType;

    if (contextTypeField != null)
    {
      contextTypeField.setText(StringUtil.getTypeText(contextType));
    }

    ITextViewer queryTextViewer = getQueryTextViewer();

    if (queryTextViewer instanceof QueryDelegateTextViewer)
    {
      ((QueryDelegateTextViewer)queryTextViewer).setContext(contextType);
    }
  }

  /**
   * Returns the value of the result type property.
   * @return the result type
   */
  protected EClassifier getResultType()
  {
    return resultType;
  }

  /**
   * Sets the value of the result type property.
   * @param resultType the value
   */
  protected void setResultType(EClassifier resultType)
  {
    this.resultType = resultType;

    if (resultTypeField != null)
    {
      resultTypeField.setText(StringUtil.getTypeText(resultType));
    }
  }

  @Override
  protected DataSetDesign collectDataSetDesign(DataSetDesign design)
  {
    if (getControl() == null) // page control was never created
    {
      return design; // no editing was done
    }

    if (!hasValidData())
    {
      return null; // trigger a design session error status
    }

    savePage(design);

    return design;
  }

  @Override
  protected boolean canLeave()
  {
    return isPageComplete();
  }

  /**
   * Validates that the user has specified values for the properties in the page control
   * and sets page message accordingly.
   */
  protected void validateData()
  {
    QueryDelegate.Factory factory = (QueryDelegate.Factory)QueryDelegate.Factory.Registry.INSTANCE.get(getQueryDelegate());
    boolean isValid = factory != null;

    if (factory != null)
    {
      EClassifier context = getContextType();
      isValid = context != null;

      if (isValid)
      {
        String queryText = getQueryText();
        isValid = !StringUtil.isEmpty(queryText);

        if (isValid)
        {
          isValid = getResultType() != null;

          if (isValid)
          {
            try
            {
              List<Variable> variables = getVariables();
              Map<String, EClassifier> map = variables.isEmpty() ? null : convertVariablesToMap(variables);
              factory.createQueryDelegate(context, map, queryText).prepare();

              setMessage(DEFAULT_MESSAGE);
            }
            catch (Exception e)
            {
              isValid = false;

              Throwable cause = e.getCause();
              setMessage(cause == null ? e.getLocalizedMessage() : cause.getLocalizedMessage(), ERROR);
            }
          }
          else
          {
            setMessage(ODAEcoreUIPlugin.INSTANCE.getString("_UI_ResultTypeCannotBeEmpty_message"), ERROR); //$NON-NLS-1$                              
          }
        }
        else
        {
          setMessage(ODAEcoreUIPlugin.INSTANCE.getString("_UI_QueryTextCannotBeEmpty_message"), ERROR); //$NON-NLS-1$        
        }
      }
      else
      {
        setMessage(ODAEcoreUIPlugin.INSTANCE.getString("_UI_ContextTypeCannotBeEmpty_message"), ERROR); //$NON-NLS-1$                  
      }
    }
    else
    {
      setMessage(ODAEcoreUIPlugin.INSTANCE.getString("_UI_QueryDelegateCannotBeEmpty_message"), ERROR); //$NON-NLS-1$
    }

    setPageComplete(isValid);
  }

  /**
   * Indicates whether the custom page has valid data to proceed 
   * with defining a data set.
   */
  protected boolean hasValidData()
  {
    validateData();

    return canLeave();
  }

  /**
   * Saves the user-defined values in this page, and updates the specified 
   * data set design with the latest design definition.
   */
  protected void savePage(DataSetDesign dataSetDesign)
  {
    // save user-defined query text
    String queryText = getQueryText();
    dataSetDesign.setQueryText(queryText);

    // obtain query's current runtime metadata, and maps it to the dataSetDesign
    IConnection connection = null;

    try
    {
      // instantiate runtime driver class
      IDriver driver = new Driver();

      // obtain and open a live connection
      connection = driver.getConnection(null);
      java.util.Properties properties = DesignSessionUtil.getEffectiveDataSourceProperties(getEditingDesign().getDataSourceDesign());
      connection.open(properties);

      // update the data set design with the 
      // query's current runtime metadata
      updateDesign(dataSetDesign, connection, queryText);
    }
    catch (OdaException oe)
    {
      // not able to get current metadata, reset previous derived metadata
      dataSetDesign.setResultSets(null);
      dataSetDesign.setParameters(null);

      oe.printStackTrace();
    }
    finally
    {
      closeConnection(connection);
    }
  }

  /**
   * Updates the given data set design with the proeprties and derived metadata
   * obtained from the ODA runtime connection.
   */
  protected void updateDesign(DataSetDesign dataSetDesign, IConnection connection, String queryText) throws OdaException
  {
    IQuery query = connection.newQuery(null);

    QuerySpecificationHelper helper = new QuerySpecificationHelper((String)null);
    QuerySpecification specification = helper.createQuerySpecification();

    specification.setProperty(Query.DELEGATE_PROPERTY_NAME, getQueryDelegate());
    specification.setProperty(Query.CONTEXT_PROPERTY_NAME, convertTypeToString(getContextType()));
    specification.setProperty(Query.TYPE_PROPERTY_NAME, convertTypeToString(getResultType()));

    specification.setParameterValue(specification.new ParameterIdentifier(ParameterMetaData.TARGET_PARAMETER_NAME, 1), null);

    List<Variable> variables = getVariables();

    specification.setProperty(Query.VARIABLES_PROPERTY_NAME, convertVariablesToString(variables));

    for (int i = 0; i < variables.size(); i++)
    {
      specification.setParameterValue(specification.new ParameterIdentifier(variables.get(i).getName(), i + 2), null);
    }

    query.setSpecification(specification);

    Properties properties = new Properties();
    properties.putAll(specification.getProperties());

    dataSetDesign.setPublicProperties(DesignUtil.convertToDesignProperties(properties));

    query.prepare(queryText);

    try
    {
      updateResultSetDesign(query.getMetaData(), dataSetDesign);
    }
    catch (OdaException oe)
    {
      // no result set definition available, reset previous derived metadata
      dataSetDesign.setResultSets(null);
      oe.printStackTrace();
    }

    // proceed to get parameter design definition
    try
    {
      updateParameterDesign(query.getParameterMetaData(), dataSetDesign);
    }
    catch (OdaException oe)
    {
      // no parameter definition available, reset previous derived metadata
      dataSetDesign.setParameters(null);
      oe.printStackTrace();
    }
  }

  /**
   * Updates the specified data set design's result set definition based on the
   * specified runtime metadata.
   * @param resultSetMetaData runtime result set metadata instance
   * @param dataSetDesign data set design instance to update
   * @throws OdaException
   */
  protected void updateResultSetDesign(IResultSetMetaData resultSetMetaData, DataSetDesign dataSetDesign) throws OdaException
  {
    ResultSetColumns columns = DesignSessionUtil.toResultSetColumnsDesign(resultSetMetaData);

    ResultSetDefinition definition = DesignFactory.eINSTANCE.createResultSetDefinition();
    definition.setResultSetColumns(columns);

    // no exception in conversion; go ahead and assign to specified data set design
    dataSetDesign.setPrimaryResultSet(definition);
    dataSetDesign.getResultSets().setDerivedMetaData(true);
  }

  /**
   * Updates the specified data set design's parameter definition based on the
   * specified runtime metadata.
   * @param parameterMetaData runtime parameter metadata instance
   * @param dataSetDesign data set design instance to update
   * @throws OdaException
   */
  protected void updateParameterDesign(IParameterMetaData parameterMetaData, DataSetDesign dataSetDesign) throws OdaException
  {
    ParameterMode mode = DesignSessionUtil.toParameterModeDesign(IParameterMetaData.parameterModeIn);
    DataSetParameters parameters = DesignSessionUtil.toDataSetParametersDesign(parameterMetaData, mode);

    // no exception in conversion; go ahead and assign to specified data set design
    dataSetDesign.setParameters(parameters);

    if (parameters == null)
    {
      return; // no parameter definitions; done with update
    }

    ParameterMetaData metaData = (ParameterMetaData)parameterMetaData;

    for (ParameterDefinition parameterDefinition : parameters.getParameterDefinitions())
    {
      DataElementAttributes attributes = parameterDefinition.getAttributes();

      if (attributes != null && attributes.isSetNativeDataTypeCode())
      {
        parameterDefinition.addDefaultValue(metaData.getParameterDefaultValue(attributes.getName()));
      }
    }

    parameters.setDerivedMetaData(true);
  }

  /**
   * Attempts to close the given ODA connection.
   */
  protected void closeConnection(IConnection connection)
  {
    try
    {
      if (connection != null && connection.isOpen())
      {
        connection.close();
      }
    }
    catch (OdaException e)
    {
      // ignore
      e.printStackTrace();
    }
  }

}
