/**
 * Copyright (c) 2002-2006 IBM Corporation 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
 * 
 * Contributors: 
 *   IBM - Initial API and implementation
 */
package org.eclipse.emf.edit.ui.provider;


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

import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.ui.views.properties.IPropertySource;
import org.eclipse.ui.views.properties.IPropertySourceProvider;
import org.eclipse.swt.widgets.Display;

import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.provider.IChangeNotifier;
import org.eclipse.emf.edit.provider.IItemPropertySource;
import org.eclipse.emf.edit.provider.INotifyChangedListener;
import org.eclipse.emf.edit.provider.IStructuredItemContentProvider;
import org.eclipse.emf.edit.provider.ITreeItemContentProvider;
import org.eclipse.emf.edit.provider.IViewerNotification;
import org.eclipse.emf.edit.provider.ViewerNotification;


/**
 * This content provider wraps an AdapterFactory 
 * and it delegates its JFace provider interfaces to corresponding adapter-implemented item provider interfaces.
 * All method calls to the various structured content provider interfaces 
 * are delegated to interfaces implemented by the adapters generated by the AdapterFactory.
 * {@link org.eclipse.jface.viewers.IStructuredContentProvider} is delegated to 
 * {@link IStructuredItemContentProvider}; {@link ITreeContentProvider} is delegated 
 * to {@link ITreeItemContentProvider};
 * and {@link IPropertySourceProvider} to {@link IItemPropertySource}.
 */
public class AdapterFactoryContentProvider 
  implements 
    ITreeContentProvider, 
    IPropertySourceProvider,
    INotifyChangedListener
{
  /**
   * This keeps track of the one factory we are using.  
   * Use a {@link org.eclipse.emf.edit.provider.ComposedAdapterFactory} if adapters 
   * from more the one factory are involved in the model.
   */
  protected AdapterFactory adapterFactory;

  /**
   * This keeps track of the one viewer using this content provider.
   */
  protected Viewer viewer;

  /**
   * This is used to queue viewer notifications and refresh viewers based on them. 
   * @since 2.2.0
   */
  protected ViewerRefresh viewerRefresh;

  private static final Class<?> IStructuredItemContentProviderClass = IStructuredItemContentProvider.class;
  private static final Class<?> ITreeItemContentProviderClass = ITreeItemContentProvider.class;
  private static final Class<?> IItemPropertySourceClass = IItemPropertySource.class;

  /**
   * This constructs an instance that wraps this factory.
   * The factory should yield adapters that implement the various IItemContentProvider interfaces.
   * If the adapter factory is an {@link IChangeNotifier},
   * a listener is added to it,
   * so it's important to call {@link #dispose()}.
   */
  public AdapterFactoryContentProvider(AdapterFactory adapterFactory)
  {
    this.adapterFactory = adapterFactory;

    if (adapterFactory instanceof IChangeNotifier)
    {
      ((IChangeNotifier)adapterFactory).addListener(this);
    }
  }

  /**
   * This sets the wrapped factory.
   * If the adapter factory is an {@link IChangeNotifier},
   * a listener is added to it,
   * so it's important to call {@link #dispose()}.
   */
  public void setAdapterFactory(AdapterFactory adapterFactory)
  {
    if (this.adapterFactory instanceof IChangeNotifier)
    {
      ((IChangeNotifier)this.adapterFactory).removeListener(this);
    }

    if (adapterFactory instanceof IChangeNotifier)
    {
      ((IChangeNotifier)adapterFactory).addListener(this);
    }

    this.adapterFactory = adapterFactory;
  }

  /**
   * This returns the wrapped factory.
   */
  public AdapterFactory getAdapterFactory()
  {
    return adapterFactory;
  }

  /**
   * The given Viewer will start (oldInput == null) or stop (newInput == null) listening for domain events.
   */
  public void inputChanged(Viewer viewer, Object oldInput, Object newInput)
  {
    // If there was no old input, then we must be providing content for this part for the first time...
    //
    this.viewer = viewer;
  }

  /**
   * This implements {@link org.eclipse.jface.viewers.IStructuredContentProvider}.getElements to 
   * forward the call to an object that implements 
   * {@link org.eclipse.emf.edit.provider.IStructuredItemContentProvider#getElements IStructuredItemContentProvider.getElements}.
   */
  public Object [] getElements(Object object)
  {
    // Get the adapter from the factory.
    //
    IStructuredItemContentProvider structuredItemContentProvider = 
      (IStructuredItemContentProvider)adapterFactory.adapt(object, IStructuredItemContentProviderClass);

    // Either delegate the call or return nothing.
    //
    return 
      (structuredItemContentProvider != null ? 
        structuredItemContentProvider.getElements(object) : 
        Collections.EMPTY_LIST).toArray();
  }

  /**
   * This implements {@link org.eclipse.jface.viewers.ITreeContentProvider}.getChildren to forward the call to an object that implements 
   * {@link org.eclipse.emf.edit.provider.ITreeItemContentProvider#getChildren ITreeItemContentProvider.getChildren}.
   */
  public Object [] getChildren(Object object)
  {
    // Get the adapter from the factory.
    //
    ITreeItemContentProvider treeItemContentProvider = 
      (ITreeItemContentProvider)adapterFactory.adapt(object, ITreeItemContentProviderClass);

    // Either delegate the call or return nothing.
    //
    return
      (treeItemContentProvider != null ?
        treeItemContentProvider.getChildren(object) :
        Collections.EMPTY_LIST).toArray();
  }

  /**
   * This implements {@link org.eclipse.jface.viewers.ITreeContentProvider}.hasChildren to forward the call to an object that implements 
   * {@link org.eclipse.emf.edit.provider.ITreeItemContentProvider#hasChildren ITreeItemContentProvider.hasChildren}.
   */
  public boolean hasChildren(Object object)
  {
    // Get the adapter from the factory.
    //
    ITreeItemContentProvider treeItemContentProvider = 
      (ITreeItemContentProvider)adapterFactory.adapt(object, ITreeItemContentProviderClass);

    // Either delegate the call or return nothing.
    //
    return 
      treeItemContentProvider != null  && 
        treeItemContentProvider.hasChildren(object);
  }

  /**
   * This implements {@link org.eclipse.jface.viewers.ITreeContentProvider}.getParent to forward the call to an object that implements 
   * {@link org.eclipse.emf.edit.provider.ITreeItemContentProvider#getParent ITreeItemContentProvider.getParent}.
   */
  public Object getParent(Object object)
  {
    // Get the adapter from the factory.
    //
    ITreeItemContentProvider treeItemContentProvider = 
      (ITreeItemContentProvider)adapterFactory.adapt(object, ITreeItemContentProviderClass);

    // Either delegate the call or return nothing.
    //
    return 
      treeItemContentProvider != null ?
        treeItemContentProvider.getParent(object) :
        null;
  }

  /**
   * This discards the content provider and removes this as a listener to the {@link #adapterFactory}.
   */
  public void dispose()
  {
    if (adapterFactory instanceof IChangeNotifier)
    {
      ((IChangeNotifier)adapterFactory).removeListener(this);
    }
    viewer = null;
  }

  /**
   * This implements {@link org.eclipse.ui.views.properties.IPropertySourceProvider}.getPropertySource to forward the call to an object that implements
   * {@link org.eclipse.emf.edit.provider.IItemPropertySource}.
   */
  public IPropertySource getPropertySource(Object object)
  {
    if (object instanceof IPropertySource)
    {
      return (IPropertySource)object;
    }
    else
    {
      IItemPropertySource itemPropertySource =
        (IItemPropertySource)
          (object instanceof EObject && ((EObject)object).eClass() == null ?
            null :
            adapterFactory.adapt(object, IItemPropertySourceClass));
  
      return 
        itemPropertySource != null ?  createPropertySource(object, itemPropertySource) : null;
    }
  }

  protected IPropertySource createPropertySource(Object object, IItemPropertySource itemPropertySource)
  {
    return new PropertySource(object, itemPropertySource);
  }

  public void notifyChanged(Notification notification)
  {
    if (viewer != null && viewer.getControl() != null && !viewer.getControl().isDisposed())
    {
      // If the notification is an IViewerNotification, it specifies how ViewerRefresh should behave.  Otherwise fall
      // back to NotifyChangedToViewerRefresh, which determines how to refresh the viewer directly from the model
      // notification.
      //
      if (notification instanceof IViewerNotification)
      {
        if (viewerRefresh == null)
        {
          viewerRefresh = new ViewerRefresh(viewer, getViewerRefreshTheshold());
        }

        if (viewerRefresh.addNotification((IViewerNotification)notification))
        {
          // If we have positive refresh delay and the notification is arriving from a background thread...
          //
          final Display display = viewer.getControl().getDisplay();
          final int viewerRefreshDelay = getViewerRefreshDelay();
          if (viewerRefreshDelay > 0 && display.getThread() != Thread.currentThread())
          {
            // Process the viewer refresh after the specified delay.
            display.asyncExec
              (new Runnable()
               {
                 public void run()
                 {
                   display.timerExec(viewerRefreshDelay, viewerRefresh);
                 }
               });
          }
          else
          {
            display.asyncExec(viewerRefresh);
          }
        }
      }
      else
      {
        NotifyChangedToViewerRefresh.handleNotifyChanged(
          viewer,
          notification.getNotifier(),
          notification.getEventType(),
          notification.getFeature(),
          notification.getOldValue(),
          notification.getNewValue(),
          notification.getPosition());
      }
    }
  }

  /**
   * Returns the value threshold value passed to {@link ViewerRefresh#ViewerRefresh(Viewer, int)},
   * which in turn determines the maximum number of queued {@link IViewerNotification viewer notifications}
   * before they are all folded into a single full viewer refresh notification.
   * The default value is 30.
   *
   * @since 2.14
   */
  protected int getViewerRefreshTheshold()
  {
    return 30;
  }

  /**
   * Returns the delay value used by {@link #notifyChanged(Notification)} to specify the time in milliseconds to delay processing of queued notifications
   * in the case that the notifications arrive from a non-display thread.
   * The default value is 200.
   *
   * @since 2.14
   */
  protected int getViewerRefreshDelay()
  {
    return 200;
  }

  /**
   * A runnable class that efficiently updates a {@link org.eclipse.jface.viewers.Viewer} via standard APIs, based on
   * queued {@link org.eclipse.emf.edit.provider.IViewerNotification}s from the model's item providers.
   */
  public static class ViewerRefresh implements Runnable
  {
    private Viewer viewer;
    private List<IViewerNotification> notifications;
    private boolean compatibility;
    private int count;
    private int threshold;

    /**
     * @since 2.2.0
     */
    public ViewerRefresh(Viewer viewer)
    {
      this(viewer, Integer.MAX_VALUE);
    }

    /**
     * @param threshold the maximum number of queued notifications before they are all folded into a single full viewer refresh.
     * @since 2.14
     */
    public ViewerRefresh(Viewer viewer, int threshold)
    {
      this.viewer = viewer;
      this.threshold = threshold;
    }

    /**
     * @deprecated in 2.2.0
     */
    @Deprecated
    public ViewerRefresh(Viewer viewer, IViewerNotification notification)
    {
      this.viewer = viewer;
      addNotification(notification);
      compatibility = true;
    }

    /**
     * Adds a viewer notification to the queue that will be processed by this <code>ViewerRefresh</code>.
     * Duplicative notifications will not be queued.
     * @param notification the notification to add to the queue 
     * @return whether the queue has been made non-empty, which would indicate that the <code>ViewerRefresh</code>
     *   needs to be {@link Display#asyncExec scheduled} on the event queue 
     * @since 2.2.0
     */
    public synchronized boolean addNotification(IViewerNotification notification)
    {
      if (notifications == null)
      {
        notifications = new ArrayList<IViewerNotification>();
        count = 0;
      }

      if (notifications.isEmpty())
      {
        ++count;
        notifications.add(notification);
        return true;
      }

      if (viewer instanceof StructuredViewer)
      {
        // If there are more than threshold notifications, it will be cheaper just to refresh the tree once.
        if (count > threshold)
        {
          if (notifications.size() != 1)
          {
            notifications.clear();
            notifications.add(new ViewerNotification(notification, null, true, true));
          }
        }
        else
        {
          for (Iterator<IViewerNotification> i = notifications.iterator(); i.hasNext() && notification != null; )
          {
            IViewerNotification old = i.next();
            IViewerNotification merged = merge(old, notification);
            if (merged == old)
            {
              notification = null;
            }
            else if (merged != null)
            {
              notification = merged;
              --count;
              i.remove();
            }
          }
          if (notification != null)
          {
            notifications.add(notification);
            ++count;
          }
        }
      }
      return false;
    }

    /**
     * Compares two notifications and, if duplicative, returns a single notification that does the work of
     * both.  Note: this gives priority to a content refresh on the whole viewer over a content refresh or
     * label update on a specific element; however, it doesn't use parent-child relationships to determine
     * if refreshes on non-equal elements are duplicative.
     * @return a single notification that is equivalent to the two parameters, or null if they are non-duplicative
     * @since 2.2.0
     */
    protected IViewerNotification merge(IViewerNotification n1, IViewerNotification n2)
    {
      // This implements the following order of preference:
      //   1. full refresh and update
      //   2. full refresh (add update if necessary)
      //   3. refresh element with update
      //   4. refresh element (if necessary)
      //   5. update element
      // 
      if (n1.getElement() == null && n1.isLabelUpdate())
      {
        return n1;
      }
      else if (n2.getElement() == null && n2.isLabelUpdate())
      {
        return n2;
      }
      else if (n1.getElement() == null)
      {
        if (n2.isLabelUpdate())
        {
          n1 = new ViewerNotification(n1);
        }
        return n1;
      }
      else if (n2.getElement() == null)
      {
        if (n1.isLabelUpdate())
        {
          n2 = new ViewerNotification(n2);
        }
        return n2;
      }
      else if (n1.getElement() == n2.getElement())
      {
        if (n1.isContentRefresh() && n1.isLabelUpdate())
        {
          return n1;
        }
        else if (n2.isContentRefresh() && n2.isLabelUpdate())
        {
          return n2;
        }
        else if (n1.isContentRefresh())
        {
          if (n2.isLabelUpdate())
          {
            n1 = new ViewerNotification(n1, n1.getElement(), true, true);
          }
          return n1;
        }
        else if (n2.isContentRefresh())
        {
          if (n1.isLabelUpdate())
          {
            n2 = new ViewerNotification(n2, n2.getElement(), true, true);
          }
          return n2;
        }
        else if (n1.isLabelUpdate())
        {
          return n1;
        }
        else // n2.isLabelUpdate()
        {
          return n2;
        }
      }
      return null;
    }

    public void run()
    {
      if (viewer != null && viewer.getControl() != null && !viewer.getControl().isDisposed())
      {
        List<IViewerNotification> current;
  
        synchronized (this)
        {
          current = notifications;
          notifications = null;
        }
  
        if (current != null)
        {
          for (IViewerNotification viewerNotification : current)
          {
            refresh(viewerNotification);
          }
        }
      }
    }

    /**
     * @since 2.2.0
     */
    protected void refresh(IViewerNotification notification)
    {
      // Previously, we never updated the viewer on a resolve.  Now we post and merge it as appropriate.
      //
      if (compatibility && notification.getEventType() == Notification.RESOLVE) return;

      Object element = notification.getElement();

      if (viewer instanceof StructuredViewer)
      {
        StructuredViewer structuredViewer = (StructuredViewer)viewer;

        ISelection selection = structuredViewer.getSelection();
        boolean isStaleSelection = AdapterFactoryEditingDomain.isStale(selection);
        if (isStaleSelection)
        {
          viewer.setSelection(StructuredSelection.EMPTY);
        }

        AbstractTreeViewer treeViewer = structuredViewer instanceof AbstractTreeViewer ? (AbstractTreeViewer)structuredViewer : null;
        List<Object> expandedElements = treeViewer == null ? Collections.emptyList() : Arrays.asList(treeViewer.getExpandedElements());
        boolean isStaleExpandedElements = AdapterFactoryEditingDomain.isStale(expandedElements);

        if (element != null)
        {
          if (notification.isContentRefresh())
          {
            structuredViewer.refresh(element, notification.isLabelUpdate());
          }
          else if (notification.isLabelUpdate())
          {
            structuredViewer.update(element, null);
          }
        }
        else
        {
          structuredViewer.refresh(notification.isLabelUpdate());
        }

        if (isStaleSelection || isStaleExpandedElements)
        {
          Object object = structuredViewer.getInput();
          EditingDomain editingDomain = AdapterFactoryEditingDomain.getEditingDomainFor(object);
          if (editingDomain == null)
          {
            for (Object child : ((IStructuredContentProvider)structuredViewer.getContentProvider()).getElements(object))
            {
              editingDomain = AdapterFactoryEditingDomain.getEditingDomainFor(child);
              if (editingDomain != null)
              {
                break;
              }
            }
          }
          if (editingDomain instanceof AdapterFactoryEditingDomain)
          {
            AdapterFactoryEditingDomain adapterFactoryEditingDomain = (AdapterFactoryEditingDomain)editingDomain;
            if (treeViewer != null && isStaleExpandedElements)
            {
              treeViewer.setExpandedElements(adapterFactoryEditingDomain.resolve(expandedElements).toArray());
            }
            if (isStaleSelection)
            {
              structuredViewer.setSelection
                (new StructuredSelection(adapterFactoryEditingDomain.resolve(((IStructuredSelection)selection).toList())), true);
            }
          }
        }
      }
      else
      {
        viewer.refresh();
      }
    }
  }
}
