/**
 * Copyright (c) 2012 Eclipse contributors and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 */
package org.eclipse.emf.ecore.xcore.ui.editor;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.emf.codegen.ecore.genmodel.GenBase;
import org.eclipse.emf.codegen.ecore.genmodel.GenFeature;
import org.eclipse.emf.codegen.ecore.genmodel.GenModel;
import org.eclipse.emf.codegen.ecore.genmodel.GenModelFactory;
import org.eclipse.emf.codegen.ecore.genmodel.GenModelPackage;
import org.eclipse.emf.codegen.ecore.genmodel.GenPackage;
import org.eclipse.emf.codegen.ecore.genmodel.provider.GenModelItemProvider;
import org.eclipse.emf.codegen.ecore.genmodel.provider.GenModelItemProviderAdapterFactory;
import org.eclipse.emf.common.command.BasicCommandStack;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EDataType;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.EcorePackage;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceImpl;
import org.eclipse.emf.ecore.util.EContentAdapter;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.xcore.XAnnotation;
import org.eclipse.emf.ecore.xcore.XNamedElement;
import org.eclipse.emf.ecore.xcore.XPackage;
import org.eclipse.emf.ecore.xcore.XcorePackage;
import org.eclipse.emf.ecore.xcore.mappings.ToXcoreMapping;
import org.eclipse.emf.ecore.xcore.mappings.XcoreMapper;
import org.eclipse.emf.ecore.xcore.services.XcoreGrammarAccess;
import org.eclipse.emf.ecore.xcore.ui.quickfix.XcoreQuickfixProvider;
import org.eclipse.emf.ecore.xcore.util.XcoreGenModelInitializer;
import org.eclipse.emf.ecore.xcore.validation.XcoreResourceValidator;
import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.domain.IEditingDomainProvider;
import org.eclipse.emf.edit.provider.AdapterFactoryItemDelegator;
import org.eclipse.emf.edit.provider.ComposedAdapterFactory;
import org.eclipse.emf.edit.provider.IItemPropertyDescriptor;
import org.eclipse.emf.edit.provider.IItemPropertySource;
import org.eclipse.emf.edit.provider.ItemPropertyDescriptorDecorator;
import org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider;
import org.eclipse.emf.edit.ui.provider.PropertyDescriptor;
import org.eclipse.emf.edit.ui.provider.PropertySource;
import org.eclipse.emf.edit.ui.provider.DiagnosticDecorator.DiagnosticDecoratorAdapter;
import org.eclipse.emf.edit.ui.view.ExtendedPropertySheetPage;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.editors.text.TextEditorActionContributor;
import org.eclipse.ui.views.properties.IPropertyDescriptor;
import org.eclipse.ui.views.properties.IPropertySheetPage;
import org.eclipse.ui.views.properties.IPropertySource;
import org.eclipse.ui.views.properties.PropertySheetPage;
import org.eclipse.xtext.Keyword;
import org.eclipse.xtext.conversion.IValueConverterService;
import org.eclipse.xtext.nodemodel.ICompositeNode;
import org.eclipse.xtext.nodemodel.ILeafNode;
import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import org.eclipse.xtext.parser.IParseResult;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.ui.editor.XtextEditor;
import org.eclipse.xtext.ui.editor.model.IXtextDocument;
import org.eclipse.xtext.ui.editor.outline.impl.EObjectNode;
import org.eclipse.xtext.util.concurrent.IUnitOfWork;

import com.google.inject.Inject;

/**
 * A derived {@link XtextEditor} that supports a properties view for the Xcore resource's {@link GenModel}.
 */
public class XcoreEditor extends XtextEditor
{
  @Inject
  private XcoreMapper mapper;

  @Inject
  private IValueConverterService valueConverterService;

  @Inject XcoreGrammarAccess xcoreGrammarAccess;

  @Inject XcoreGenModelInitializer genModelInitializer;

  protected List<PropertySheetPage> propertySheetPages = new ArrayList<PropertySheetPage>();

  protected ComposedAdapterFactory adapterFactory;

  @Override
  @SuppressWarnings("all")
  public Object getAdapter(Class type)
  {
    if (type.equals(IPropertySheetPage.class))
    {
      return getPropertySheetPage();
    }
    else
    {
      return super.getAdapter(type);
    }
  }

  public IPropertySheetPage getPropertySheetPage()
  {
    // Create an adapter factory that uses registered item provider adapter factories, but specializes the one for the GenModel
    //
    adapterFactory = new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE);
    final AdapterFactoryItemDelegator itemDelegator = new AdapterFactoryItemDelegator(adapterFactory);
    adapterFactory.addAdapterFactory
      (new GenModelItemProviderAdapterFactory()
       {
         @Override
         public Adapter createGenModelAdapter()
         {
           // Create a new adapter each time.
           //
           return
             new GenModelItemProvider(this)
             {
               @Override
               public List<IItemPropertyDescriptor> getPropertyDescriptors(Object object)
               {
                 if (itemPropertyDescriptors == null)
                 {
                   super.getPropertyDescriptors(object);

                   // Merge in the decorated property descriptors for the one GenPackage in the model.
                   //
                   GenModel genModel = (GenModel)object;
                   EList<GenPackage> genPackages = genModel.getGenPackages();
                   if (!genPackages.isEmpty())
                   {
                     GenPackage genPackage = genPackages.get(0);
                     List<IItemPropertyDescriptor> genPackagePropertyDescriptors = itemDelegator.getPropertyDescriptors(genPackage);
                     for (IItemPropertyDescriptor genPackagePropertyDescriptor : genPackagePropertyDescriptors)
                     {
                       itemPropertyDescriptors.add(new ItemPropertyDescriptorDecorator(genPackage, genPackagePropertyDescriptor));
                     }
                   }
                 }
                 return itemPropertyDescriptors;
               }
             };
           }
         });

    // Cache the editor's document and resource set.
    //
    final IXtextDocument document = getDocument();
    final ResourceSet resourceSet =
      document.readOnly
        (new IUnitOfWork<ResourceSet, XtextResource>()
         {
           public ResourceSet exec(final XtextResource xtextResource) throws Exception
           {
             return xtextResource.getResourceSet();
           }
         });

    // Create a specialized command stack.
    //
    BasicCommandStack commandStack =
      new BasicCommandStack()
      {
        protected boolean isDefault(final GenModel genModel, EObject eObject, EStructuralFeature eStructuralFeature)
        {
          // Create a freshly initialized GenModel for the original GenModel's EPackage.
          //
          Resource fakeResource = new ResourceImpl(genModel.eResource().getURI());
          final GenModel clonedGenModel = GenModelFactory.eINSTANCE.createGenModel();
          EList<GenPackage> genPackages = genModel.getGenPackages();
          if (!genPackages.isEmpty())
          {
            clonedGenModel.initialize(Collections.singleton(genPackages.get(0).getEcorePackage()));
          }
          genModelInitializer.initialize(clonedGenModel, false);
          fakeResource.getContents().add(clonedGenModel);

          // Traverse the EObject to find its clone in the cloned GenModel.
          //
          EObject clonedEObject =
            new Object()
            {
              EObject traverse(EObject eObject)
              {
                EObject eContainer = eObject.eContainer();
                if (eContainer == null)
                {
                  return clonedGenModel;
                }
                else
                {
                  EReference eContainmentFeature = eObject.eContainmentFeature();
                  EObject clonedEObject = traverse(eContainer);
                  Object value = clonedEObject.eGet(eContainmentFeature);
                  if (eContainmentFeature.isMany())
                  {
                    @SuppressWarnings("unchecked")
                    List<EObject> values = (List<EObject>)value;
                    List<?> originalValues = (List<?>)eContainer.eGet(eContainmentFeature);
                    return values.get(originalValues.indexOf(eObject));
                  }
                  else
                  {
                    return (EObject)value;
                  }
                }
              }
            }.traverse(eObject);

          // Test whether the feature's value is the same as the default.
          //
          Object newValue = eObject.eGet(eStructuralFeature);
          Object defaultValue = clonedEObject.eGet(eStructuralFeature);
          return
            newValue == null ?
              defaultValue == null :
              newValue instanceof GenFeature  && defaultValue instanceof GenFeature ?
                ((GenFeature)newValue).getEcoreFeature() == ((GenFeature)defaultValue).getEcoreFeature() :
                newValue.equals(defaultValue);
        }

        protected String getValue(EObject eObject, EStructuralFeature eStructuralFeature)
        {
          EClassifier eType = eStructuralFeature.getEType();
          Object value = eObject.eGet(eStructuralFeature);
          String literal =
            eType instanceof EDataType ?
              getEDataTypeLiteral(value, (EDataType)eType, eStructuralFeature.isMany()) :
              ((GenFeature)value).getName();
          return valueConverterService.toString(literal, "STRING");
        }

        protected String getEDataTypeLiteral(Object value, EDataType eDataType, boolean isMany)
        {
          if (isMany)
          {
            StringBuilder result = new StringBuilder();
            for (Object element : (List<?>)value)
            {
              if (result.length() != 0)
              {
                result.append(" ");
              }
              result.append(EcoreUtil.convertToString(eDataType, element));
            }
            return result.toString();
          }
          else
          {
            return EcoreUtil.convertToString(eDataType, value);
          }
        }

        @Override
        public void execute(Command command)
        {
          // Before executing the command, add a content adapter to the GenModel to be notified of whatever feature changes.
          //
          final Resource resource = resourceSet.getResources().get(0);
          final GenModel genModel = (GenModel)EcoreUtil.getObjectByType(resource.getContents(), GenModelPackage.Literals.GEN_MODEL);
          final List<Notification> notifications = new ArrayList<Notification>();
          final EContentAdapter eContentAdatper =
            new EContentAdapter()
            {
              @Override
              public void notifyChanged(final Notification notification)
              {
                super.notifyChanged(notification);
                int eventType = notification.getEventType();
                if (eventType == Notification.REMOVING_ADAPTER)
                {
                  // If we are removing the adapters from the GenModel, because it's unloaded when a new GenModel is inferred...
                  //
                  if (notification.getNotifier() instanceof GenModel)
                  {
                    // Defer producing a new selection changed event to update the properties view for the the new inferred selection.
                    //
                    getEditorSite().getShell().getDisplay().asyncExec
                      (new Runnable()
                       {
                         public void run()
                         {
                           document.readOnly
                             (new IUnitOfWork.Void<XtextResource>()
                              {
                                @Override
                                public void process(final XtextResource xtextResource) throws Exception
                                {
                                  ISelection selection = getSourceViewer().getSelectionProvider().getSelection();
                                  for (Iterator<PropertySheetPage> i = propertySheetPages.iterator(); i.hasNext(); )
                                  {
                                    PropertySheetPage propertySheetPage = i.next();
                                    if (propertySheetPage.getControl() == null || propertySheetPage.getControl().isDisposed())
                                    {
                                      i.remove();
                                    }
                                    else
                                    {
                                      propertySheetPage.selectionChanged(XcoreEditor.this, selection);
                                    }
                                  }
                                }
                              });
                         }
                       });
                  }
                }
                else if (!notification.isTouch() && notification.getNotifier() instanceof EObject)
                {
                  // Record the notifications.
                  //
                  notifications.add(0, notification);
                }
              }
            };
          genModel.eAdapters().add(eContentAdatper);

          // Execute the command, recording notifications so they can be processed after the command is complete.
          //
          super.execute(command);

          // Process the deferred notifications.
          //
          if (!notifications.isEmpty())
          {
            final Notification notification = notifications.get(0);

            // For the feature of the object that's changed, process the new contents of the feature.
            //
            document.modify
              (new IUnitOfWork.Void<XtextResource>()
               {
                 @Override
                 public void process(XtextResource state) throws Exception
                 {
                   // Determine the object and feature that are changed.
                   //
                   EObject eObject = (EObject)notification.getNotifier();
                   EStructuralFeature eStructuralFeature = (EStructuralFeature)notification.getFeature();
                   String name = eStructuralFeature.getName();

                   // Determine the affected Xcore element.
                   //
                   ToXcoreMapping xcoreMapping = mapper.getToXcoreMapping(eObject);
                   XNamedElement xNamedElement = xcoreMapping.getXcoreElement();
                   if (xNamedElement == null && eObject instanceof GenModel)
                   {
                     xNamedElement = (XPackage)resource.getContents().get(0);
                   }
                   if (xNamedElement != null)
                   {
                     // Determine the nodes affected for the element,  i.e.,
                     // the node for the element as a whole,
                     // the node for the annotation,
                     // the node for the detail entry,
                     // and the node for the value in the detail entry.
                     //
                     ICompositeNode elementNode = NodeModelUtils.getNode(xNamedElement);
                     ICompositeNode annotationNode = null;
                     ICompositeNode detailNode = null;
                     List<INode> valueNodes = null;

                     // Determine if there is already an annotation for the GenModel's annotation URI.
                     //
                     XAnnotation xAnnotation = xNamedElement.getAnnotation(GenModelPackage.eNS_URI);
                     if (xAnnotation != null)
                     {
                       // If there is, get the node for that.
                       //
                       annotationNode = NodeModelUtils.getNode(xAnnotation);

                       // Determine if there is a detail entry for the affected feature.
                       //
                       for (Map.Entry<String, String> detail : xAnnotation.getDetails())
                       {
                         if (name.equals(detail.getKey()))
                         {
                           // If there is a matching key, determine the overall node for it and the node for the value.
                           //
                           detailNode = NodeModelUtils.findActualNodeFor((EObject)detail);
                           valueNodes = NodeModelUtils.findNodesForFeature((EObject)detail, XcorePackage.Literals.XSTRING_TO_STRING_MAP_ENTRY__VALUE);
                           break;
                         }
                       }
                     }
                     // If we found a node for the element...
                     //
                     if (elementNode != null)
                     {
                       // If there doesn't yet exist an annotation node.
                       //
                       if (annotationNode == null)
                       {
                         // Insert a new annotation with the key/value mapping on a new line before the element node.
                         //
                         int offset = elementNode.getOffset();

                         // Match the indentation of the element.
                         //
                         int line = document.getLineOfOffset(offset);
                         String lineDelimiter = document.getLineDelimiter(line);
                         int lineOffset = document.getLineOffset(line);
                         String indentation = document.get(lineOffset, offset - lineOffset);
                         int length = indentation.length();
                         StringBuilder newIndentation = new StringBuilder(length);
                         for (int i = 0; i < length; ++i)
                         {
                           int codePoint = indentation.codePointAt(i);
                           newIndentation.appendCodePoint(Character.isSpaceChar(codePoint) ? codePoint : ' ');
                         }

                         document.replace(lineOffset, 0, newIndentation + "@GenModel(" + name + "=" + getValue(eObject, eStructuralFeature) + ")" + lineDelimiter);
                       }
                       // If there is a node for the old value...
                       //
                       else if (valueNodes != null)
                       {
                         // If the feature isn't set to the default, and there is a node for the detail entry, we want to remove the node...
                         //
                         if (detailNode != null && isDefault(genModel, eObject, eStructuralFeature))
                         {
                           // Cache the grammar rules we'll need to match.
                           //
                           Keyword comma = xcoreGrammarAccess.getXAnnotationAccess().getCommaKeyword_2_2_0();
                           Keyword leftParenthesis = xcoreGrammarAccess.getXAnnotationAccess().getLeftParenthesisKeyword_2_0();
                           Keyword rightParenthesis = xcoreGrammarAccess.getXAnnotationAccess().getRightParenthesisKeyword_2_3();

                           // Compute the locations of the surrounding mark-up, i.e.,
                           // the left parenthesis of the annotation,
                           // the comma before the detail entry,
                           // the start of the next entry,
                           // and the right parenthesis.
                           //
                           int leftParenthesisOffset = -1;
                           int commaOffset = -1;
                           int nextDetailNodeOffset = -1;
                           int rightParenthesisOffset = -1;

                           // This is set to true once we've iterated past the detail entry.
                           //
                           boolean matched = false;
                           for (INode child : detailNode.getParent().getChildren())
                           {
                             EObject grammarElement = child.getGrammarElement();
                             if (matched)
                             {
                               if (grammarElement == rightParenthesis)
                               {
                                 rightParenthesisOffset = child.getOffset();
                                 break;
                               }
                               else if (NodeModelUtils.findActualSemanticObjectFor(child) instanceof Map.Entry)
                               {
                                 nextDetailNodeOffset = child.getOffset();
                                 break;
                               }
                             }
                             else if (child == detailNode)
                             {
                               matched = true;
                             }
                             else if (grammarElement == leftParenthesis)
                             {
                               leftParenthesisOffset = child.getOffset();
                             }
                             else if (grammarElement == comma)
                             {
                               commaOffset = child.getOffset();
                             }
                           }

                           if (commaOffset != -1)
                           {
                             if (rightParenthesisOffset != -1)
                             {
                               // @GenModel(a="x", b="y", c="z")
                               //                       ^      ^
                               //
                               document.replace(commaOffset, rightParenthesisOffset - commaOffset, "");
                             }
                             else // if (nextDetailNodeOffset != -1)
                             {
                               // @GenModel(a="x", b="y", c="z")
                               //                 ^       ^
                               //
                               document.replace(commaOffset + 1, nextDetailNodeOffset - commaOffset - 1, " ");
                             }
                           }
                           else  // if (leftParenthesisOffset != -1)
                           {
                             if (rightParenthesisOffset != -1)
                             {
                               // @GenModel(a="x")
                               //          ^     ^
                               //
                               // document.replace(leftParenthesisOffset, rightParenthesisOffset - leftParenthesisOffset + 1, "");
                               XcoreQuickfixProvider.RemovalRegion removalRegion = new XcoreQuickfixProvider.RemovalRegion(document, xAnnotation);
                               document.replace(removalRegion.getDeleteBegin(), removalRegion.getDeleteEnd() - removalRegion.getDeleteBegin(), "");
                             }
                             else // if (nextDetailNodeOffset != -1)
                             {
                               // @GenModel(a="x", b="y", c="z")
                               //           ^      ^
                               //
                               document.replace(leftParenthesisOffset + 1, nextDetailNodeOffset - leftParenthesisOffset - 1, "");
                             }
                           }
                         }
                         else
                         {
                           // Replace the old value with the new value.
                           // @GenModel(a="x")
                           //             ^ ^
                           //
                           INode valueNode = valueNodes.get(0);
                           document.replace(valueNode.getOffset(), valueNode.getLength(), getValue(eObject, eStructuralFeature));
                         }
                       }
                       // If this is the first detail entry...
                       //
                       else if (xAnnotation != null && xAnnotation.getDetails().isEmpty())
                       {
                         // Add the key/value mapping with new parentheses.
                         // @GenModel
                         //          ^
                         //
                         int offset = annotationNode.getOffset() + annotationNode.getLength();
                         document.replace(offset, 0, "(" + name + "=" + getValue(eObject, eStructuralFeature) + ")");
                       }
                       // Otherwise, we just need to add a new key/value mapping to the end of the list.
                       // @GenModel(a="x")
                       //                ^
                       //
                       else
                       {
                         int offset = annotationNode.getOffset() + annotationNode.getLength() - 1;
                         document.replace(offset, 0, ", " + name + "=" + getValue(eObject, eStructuralFeature));
                       }
                     }
                   }
                 }
               });
          }
        }
      };

    // Create the editing domain with a special command stack.
    // Be sure that only objects in the main resource are modifiable.
    //
    final AdapterFactoryEditingDomain editingDomain =
      new AdapterFactoryEditingDomain(adapterFactory, commandStack, resourceSet)
      {
        @Override
        public boolean isReadOnly(Resource resource)
        {
          return super.isReadOnly(resource) || getResourceSet().getResources().indexOf(resource) != 0;
        }
      };

    // Ensure that the editing domain for the resource set can be determined.
    //
    class EditingDomainProvider extends AdapterImpl implements IEditingDomainProvider
    {
      public EditingDomain getEditingDomain()
      {
        return editingDomain;
      }
      @Override
      public boolean isAdapterForType(Object type)
      {
        return IEditingDomainProvider.class.equals(type);
      }
    }
    resourceSet.eAdapters().add(new EditingDomainProvider());

    // The property sheet does manual validation, but the resource validation is done automatically.
    // Inform the diagnostic decorator adapter about the results of the resource validation.
    resourceSet.eAdapters().add(new XcoreResourceValidator.ValidationAdapter()
      {
        @Override
        public void update(Diagnostic diagnostic)
        {
          DiagnosticDecoratorAdapter.update(resourceSet, diagnostic);
        }
      });

    // Create a specialized property sheet page.
    //
    final PropertySheetPage propertySheetPage =
      new ExtendedPropertySheetPage(editingDomain, ExtendedPropertySheetPage.Decoration.MANUAL)
      {
        protected DelayedProcessor<Control, ITextSelection> delayedProcessor;

        @Override
        public void createControl(Composite parent)
        {
          super.createControl(parent);

          delayedProcessor =
            new DelayedProcessor<Control, ITextSelection>(getControl(), 1000)
            {
              protected long lastModification;
              
              {
                getSourceViewer().getTextWidget().addModifyListener
                  (new ModifyListener()
                   {
                     public void modifyText(ModifyEvent modifyEvent)
                     {
                       // Keep track of the last time the text was modified.
                       // This way we can make the delayed response of the properties view kick in only if there is a recent modification.
                       //
                       lastModification = System.currentTimeMillis();
                     }
                   });
              }

              @Override
              protected boolean isStale(ITextSelection newValue)
              {
                return newValue.getOffset() != getSourceViewer().getTextWidget().getSelection().x;
              }
              
              @Override
              public void delayedProcess(ITextSelection value)
              {
                // Don't delay processing if the text has not recently been modified.
                //
                if (runnable == null && (System.currentTimeMillis() - lastModification) > 2000)
                {
                  process(value);
                }
                else
                {
                  super.delayedProcess(value);
                }
              }

              @Override
              protected void process(final ITextSelection textSelection)
              {
                IXtextDocument document = getDocument();
                document.readOnly
                  (new IUnitOfWork.Void<XtextResource>()
                   {
                     @Override
                     public void process(XtextResource xtextResource) throws Exception
                     {
                       IParseResult parseResult = xtextResource.getParseResult();
                       if (parseResult != null)
                       {
                         ICompositeNode rootNode = parseResult.getRootNode();
                         if (rootNode != null)
                         {
                           ILeafNode node = NodeModelUtils.findLeafNodeAtOffset(rootNode, textSelection.getOffset());
                           if (node != null)
                           {
                             // Determine the EObject and process that instead.
                             //
                             EObject eObject = NodeModelUtils.findActualSemanticObjectFor(node);
                             if (!selectionChanged(XcoreEditor.this, eObject))
                             {
                               if (AdapterFactoryEditingDomain.isStale(getInput()))
                               {
                                 selectionChanged(XcoreEditor.this, StructuredSelection.EMPTY);
                               }
                             }
                           }
                         }
                       }
                     }
                   });
              }
            };
        }

        @Override
        public void setSelectionToViewer(List<?> selection)
        {
          XcoreEditor.this.setFocus();
        }

        @Override
        public void setActionBars(IActionBars actionBars)
        {
          // Ensure that the undo/redo actions are hooked up.
          //
          super.setActionBars(actionBars);
          TextEditorActionContributor actionBarContributor = (TextEditorActionContributor)getEditorSite().getActionBarContributor();
          IActionBars editorActionBars = actionBarContributor.getActionBars();
          actionBars.setGlobalActionHandler(ActionFactory.UNDO.getId(), editorActionBars.getGlobalActionHandler((ActionFactory.UNDO.getId())));
          actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(), editorActionBars.getGlobalActionHandler((ActionFactory.REDO.getId())));
        }

        @Override
        public void selectionChanged(final IWorkbenchPart part, ISelection selection)
        {
          if (selection instanceof IStructuredSelection)
          {
            // If the first element is an EObjectNode from the outline view...
            //
            Object element = ((IStructuredSelection)selection).getFirstElement();
            if (element instanceof EObjectNode)
            {
              final EObjectNode eObjectNode = (EObjectNode)element;
              IXtextDocument document = getDocument();
              if (!document.readOnly
                    (new IUnitOfWork<Boolean, XtextResource>()
                     {
                       public Boolean exec(XtextResource xtextResource) throws Exception
                       {
                         // Determine the EObject and process that instead.
                         //
                         EObject eObject = eObjectNode.getEObject(xtextResource);
                         return selectionChanged(part, eObject);
                       }
                     }))
              {
                if (AdapterFactoryEditingDomain.isStale(getInput()))
                {
                  super.selectionChanged(part, StructuredSelection.EMPTY);
                }
              }
            }
            else
            {
              super.selectionChanged(part, selection);
            }
          }
          else if (selection instanceof ITextSelection)
          {
            // Map the selection to a model object...
            //
            final ITextSelection textSelection = (ITextSelection)selection;
            delayedProcessor.delayedProcess(textSelection);
          }
          else
          {
            super.selectionChanged(part, selection);
          }
        }

        /**
         * A helper utility for processing an EObject to determine the appropriate GenModel element to select.
         * Returns whether candidate was successfully determined.
         */
        protected boolean selectionChanged(IWorkbenchPart part, EObject eObject)
        {
          if (eObject instanceof XNamedElement)
          {
            GenBase genBase = mapper.getGen((XNamedElement)eObject);
            if (genBase instanceof GenPackage)
            {
              genBase = ((GenPackage)genBase).getGenModel();
            }
            if (genBase != null)
            {
              selectionChanged(part, new StructuredSelection(genBase));
              return true;
            }
          }
          else if (eObject instanceof GenBase)
          {
            selectionChanged(part, new StructuredSelection(eObject));
            return true;
          }
          else if (eObject != null)
          {
            return selectionChanged(part, eObject.eContainer());
          }

          return false;
        }
      };

    // Set the content provider.
    //
    final AdapterFactoryContentProvider contentProvider = new AdapterFactoryContentProvider(adapterFactory)
    {
      @Override
      protected IPropertySource createPropertySource(Object object, IItemPropertySource itemPropertySource)
      {
        return 
          new PropertySource(object, itemPropertySource)
          {
            @Override
            protected IPropertyDescriptor createPropertyDescriptor(IItemPropertyDescriptor itemPropertyDescriptor)
            {
              return 
                new PropertyDescriptor(object, itemPropertyDescriptor)
                {
                  @Override
                  public CellEditor createPropertyEditor(Composite composite)
                  {
                    if (object instanceof IItemPropertySource)
                    {
                      Object value = ((IItemPropertySource) object).getEditableValue(object);
                      if (value instanceof EObject && ((EObject)value).eClass().getEPackage() == EcorePackage.eINSTANCE)
                      {
                        // Ensure that Ecore properties are read only.
                        //
                        return null;
                      }
                    }
                    
                    return super.createPropertyEditor(composite);
                  }
                };
            }
          };
      }
    };
    propertySheetPage.setPropertySourceProvider(contentProvider);

    // Set the initial selection.
    //
    getEditorSite().getShell().getDisplay().asyncExec
      (new Runnable()
       {
         public void run()
         {
           propertySheetPage.selectionChanged(XcoreEditor.this, getSourceViewer().getSelectionProvider().getSelection());
         }
       });

    propertySheetPages.add(propertySheetPage);

    return propertySheetPage;
  }

  @Override
  public void dispose()
  {
    super.dispose();

    if (adapterFactory != null)
    {
      adapterFactory.dispose();
    }

    for (PropertySheetPage propertySheetPage : propertySheetPages)
    {
      propertySheetPage.dispose();
    }
  }

  public static abstract class DelayedProcessor<C extends Control, V>
  {
    protected Runnable runnable;
    protected int delay;
    protected C control;
    protected V value;

    public DelayedProcessor(C control, int delay)
    {
      this.control = control;
      this.delay = delay;
    }

    public void delayedProcess(V value)
    {
      if (isChanged(this.value, value) && !isStale(value))
      {
        this.value = value;
        runnable =
          new Runnable()
          {
            public void run()
            {
              if (runnable == this && !control.isDisposed() && !isStale(DelayedProcessor.this.value))
              {
                runnable = null;
                process(DelayedProcessor.this.value);
              }
            }
          };

        control.getDisplay().timerExec(delay, runnable);
      }
    }
    
    protected boolean isChanged(V oldValue, V newValue)
    {
      return oldValue == null ? newValue != null : !oldValue.equals(newValue);
    }
    
    protected boolean isStale(V newValue)
    {
      return false;
    }
    
    protected abstract void process(V value);
  }
}
