/**
 * Copyright (c) 2004-2007 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.provider;


import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CommandWrapper;
import org.eclipse.emf.common.command.UnexecutableCommand;
import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.edit.command.CommandActionDelegate;
import org.eclipse.emf.edit.command.CommandParameter;
import org.eclipse.emf.edit.command.DragAndDropCommand;
import org.eclipse.emf.edit.command.SetCommand;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.provider.IItemPropertyDescriptor.OverrideableCommandOwner;


/**
 * A wrapper for model objects and other wrappers.  This handles most of the item provider methods by delegating to the
 * item provider returned by adapting on the value, but it returns the {@link org.eclipse.emf.edit.provider.WrapperItemProvider#getParent(Object) owner} 
 * as the parent, and it has to decorate the children, property descriptors, and commands that it returns.
 */
public class DelegatingWrapperItemProvider extends WrapperItemProvider
  implements
    IStructuredItemContentProvider,
    ITreeItemContentProvider,
    IItemLabelProvider,
    IItemFontProvider,
    IItemColorProvider,
    ITableItemLabelProvider,
    ITableItemFontProvider,
    ITableItemColorProvider,
    IItemPropertySource,
    IEditingDomainItemProvider,
    IChangeNotifier,
    INotifyChangedListener
{
  /**
   * The wrapped value's item provider, to which most methods are delegated.
   */
  protected Object delegateItemProvider;

  /**
   * The wrapped children are cached here, keyed by the children returned by the delegate item provider.
   */
  protected Map<Object, IWrapperItemProvider> childrenMap;

  /**
   * The collection of children last returned by the delegate item provider is cached here.
   */
  protected Collection<?> delegateChildren;

  /**
   * The decorated property descriptors are cached here.
   */
  protected List<IItemPropertyDescriptor> propertyDescriptors;

  /**
   * Records any listeners for this wrapper and fires notifications to them.
   */
  protected IChangeNotifier changeNotifier;

  /**
   * Creates an instance for the given value. A decorator for the object's item provider is created, and
   * set up to repeat notifications, decorating them, so that they will update this wrapper, rather than the model
   * object they originate from.
   * If the adapter factory is an {@link IChangeNotifier},
   * a listener is added to it,
   * so it's important to call {@link #dispose()}.
   * 
   * @exception IllegalArgumentException If the specified value is null.
   */
  public DelegatingWrapperItemProvider(Object value, Object owner, EStructuralFeature feature, int index, AdapterFactory adapterFactory)
  {
    super(value, owner, feature, index, adapterFactory);

    if (value == null)
    {
      throw new IllegalArgumentException("value=null");
    }

    Object delegateValue = getDelegateValue();
    if (delegateValue != null)
    {
      delegateItemProvider = getRootAdapterFactory().adapt(delegateValue, IStructuredItemContentProvider.class);
      if (delegateItemProvider instanceof IChangeNotifier)
      {
        ((IChangeNotifier)delegateItemProvider).addListener(this);
      }
    }
  }

  /**
   * Creates an instance for the given value, when the feature and index that specify the value are unknown. 
   * 
   * @exception IllegalArgumentException If the specified value is null.
   * 
   * @deprecated As of EMF 2.0.1, replaced by {@link #DelegatingWrapperItemProvider(Object, org.eclipse.emf.ecore.EObject, EStructuralFeature, int, AdapterFactory)
   * this form}. This constructor will be removed as public API, but remain available as a protected method.
   */
  @Deprecated
  public DelegatingWrapperItemProvider(Object value, Object owner, AdapterFactory adapterFactory)
  {
    this(value, owner, null, CommandParameter.NO_INDEX, adapterFactory);
  }

  /**
   * Deactivates notification repeating and disposes any wrappers it is maintaining for its children.
   */
  @Override
  public void dispose()
  {
    if (delegateItemProvider instanceof IChangeNotifier)
    {
      ((IChangeNotifier)delegateItemProvider).removeListener(this);
    }

    if (childrenMap != null)
    {
      for (IDisposable object : childrenMap.values())
      {
        object.dispose();
      }
    }
  }

  /**
   * Returns the value from which to obtain and which to pass to a delegate item provider. If this returns null,
   * no delegate item provider should ever be obtained. This implementation simply returns the value of the wrapper,
   * though subclasses may override it to return something else.
   */
  protected Object getDelegateValue()
  {
    return value;
  }

  /**
   * Uses the delegate item provider to return the delegate value's elements.
   */
  @Override
  public Collection<?> getElements(Object object)
  {
    return delegateItemProvider instanceof IStructuredItemContentProvider ?
      ((IStructuredItemContentProvider)delegateItemProvider).getElements(getDelegateValue()) :
      Collections.emptyList();
  }

  /**
   * Uses the delegate item provider to return the delegate value's children, with appropriate wrappers to ensure that
   * this wrapper is considered their parent. Each child is replaced by the corresponding wrapper from {@link
   * #childrenMap}, after updating it by calling {@link #updateChildren updateChildren}.
   */
  @Override
  public Collection<?> getChildren(Object object)
  {
    updateChildren();

    Collection<Object> result = new ArrayList<Object>(delegateChildren.size());
    for (Object delegateChild : delegateChildren)
    {
      result.add(childrenMap.get(delegateChild));
    }
    return result;
  }

  /**
   * Uses the delegate item provider to get the delegate value's children, assigning the collection to {@link
   * #delegateChildren}, and to update the {@link #childrenMap}. New children are wrapped by calling {@link
   * #createWrapper createWrapper} and added to the map; Wrappers for children that have been removed are disposed. 
   */
  protected void updateChildren()
  {
    if (delegateItemProvider instanceof ITreeItemContentProvider)
    {
      boolean changed = false;
      Set<Object> oldDelegateChildren = delegateChildren != null ? new HashSet<Object>(delegateChildren) : Collections.emptySet();
      delegateChildren = ((ITreeItemContentProvider)delegateItemProvider).getChildren(getDelegateValue());

      if (childrenMap == null && !delegateChildren.isEmpty())
      {
        childrenMap = new HashMap<Object, IWrapperItemProvider>();
      }
      
      // Wrap any new children and add them to the map. Remove each current child from the set of old children.
      //
      for (Object child : delegateChildren)
      {
        
        if (!childrenMap.containsKey(child))
        {
          IWrapperItemProvider wrapper = createWrapper(child, this, adapterFactory);
          childrenMap.put(child, wrapper);
          changed = true;
        }
        oldDelegateChildren.remove(child);
      }

      // Remove and dispose any wrappers for remaining old children.
      //
      if (!oldDelegateChildren.isEmpty())
      {
        changed = true;

        for (Object child : oldDelegateChildren)
        {
          IWrapperItemProvider wrapper = childrenMap.remove(child);
          if (wrapper != null)
          {
            wrapper.dispose();
          }
        }
      }

      // If any children were added or removed, reset the indices.
      if (changed)
      {
        int index = 0;
        for (Object delegateChild : delegateChildren)
        {
          childrenMap.get(delegateChild).setIndex(index++);
        }
      }
    }
    else
    {
      delegateChildren = Collections.emptyList();
    }
  }

  /**
   * Creates a new instance of this wrapper for the given value, owner, and adapter factory.
   */
  protected IWrapperItemProvider createWrapper(Object value, Object owner, AdapterFactory adapterFactory)
  {
    return new DelegatingWrapperItemProvider(value, owner, adapterFactory);
  }

  /**
   * Uses the delegate item provider to test whether the delegate value has children.
   */
  @Override
  public boolean hasChildren(Object object)
  {
    return delegateItemProvider instanceof ITreeItemContentProvider ?
      ((ITreeItemContentProvider)delegateItemProvider).hasChildren(getDelegateValue()) :
      false;
  }

  /**
   * Uses the delegate item provider to return the delegate value's text.
   */
  @Override
  public String getText(Object object)
  {
    return delegateItemProvider instanceof IItemLabelProvider ?
      ((IItemLabelProvider)delegateItemProvider).getText(getDelegateValue()) :
      null;
  }

  /**
   * Uses the delegate item provider to return the delegate value's image.
   */
  @Override
  public Object getImage(Object object)
  {
    return delegateItemProvider instanceof IItemLabelProvider ?
      ((IItemLabelProvider)delegateItemProvider).getImage(getDelegateValue()) :
      null;
  }

  /**
   * Uses the delegate item provider to return the delegate value's font.
   */
  @Override
  public Object getFont(Object object)
  {
    return
      delegateItemProvider instanceof IItemFontProvider ?
        ((IItemFontProvider)delegateItemProvider).getFont(getDelegateValue()) :
        null;
  }

  /**
   * Uses the delegate item provider to return the delegate value's foreground color.
   */
  @Override
  public Object getForeground(Object object)
  {
    return
      delegateItemProvider instanceof IItemColorProvider ?
        ((IItemColorProvider)delegateItemProvider).getForeground(getDelegateValue()) :
        null;
  }

  /**
   * Uses the delegate item provider to return the delegate value's background color.
   */
  @Override
  public Object getBackground(Object object)
  {
    return
      delegateItemProvider instanceof IItemColorProvider ?
        ((IItemColorProvider)delegateItemProvider).getBackground(getDelegateValue()) :
        null;
  }

  /**
   * Uses the delegate item provider to return the delegate value's column text.
   */
  public String getColumnText(Object object, int columnIndex)
  {
    return 
      delegateItemProvider instanceof ITableItemLabelProvider ?
        ((ITableItemLabelProvider)delegateItemProvider).getColumnText(getDelegateValue(), columnIndex) :
        getText(object);
  }

  /**
   * Uses the delegate item provider to return the delegate value's column image.
   */
  public Object getColumnImage(Object object, int columnIndex)
  {
    return
      delegateItemProvider instanceof ITableItemLabelProvider ?
        ((ITableItemLabelProvider)delegateItemProvider).getColumnImage(getDelegateValue(), columnIndex) :
        getImage(object);
  }

  /**
   * Uses the delegate item provider to return the delegate value's font.
   */
  public Object getFont(Object object, int columnIndex)
  {
    return
      delegateItemProvider instanceof ITableItemFontProvider ?
        ((ITableItemFontProvider)delegateItemProvider).getFont(getDelegateValue(), columnIndex) :
        getFont(object);
  }

  /**
   * Uses the delegate item provider to return the delegate value's foreground color.
   */
  public Object getForeground(Object object, int columnIndex)
  {
    return 
      delegateItemProvider instanceof ITableItemColorProvider ?
        ((ITableItemColorProvider)delegateItemProvider).getForeground(getDelegateValue(), columnIndex) :
        getFont(object);
  }

  /**
   * Uses the delegate item provider to return the delegate value's background color.
   */
  public Object getBackground(Object object, int columnIndex)
  {
    return
      delegateItemProvider instanceof ITableItemColorProvider ?
        ((ITableItemColorProvider)delegateItemProvider).getBackground(getDelegateValue(), columnIndex) :
        getFont(object);
  }

  /**
   * Wraps the property descriptors returned by the delegate item provider, caching and returning them.
   */
  @Override
  public List<IItemPropertyDescriptor> getPropertyDescriptors(Object object)
  {
    if (propertyDescriptors == null)
    {
      if (delegateItemProvider instanceof IItemPropertySource)
      {
        List<IItemPropertyDescriptor> l = ((IItemPropertySource)delegateItemProvider).getPropertyDescriptors(getDelegateValue());
        propertyDescriptors = new ArrayList<IItemPropertyDescriptor>(l.size());

        for (IItemPropertyDescriptor desc : l)
        {
          propertyDescriptors.add(new DelegatingWrapperItemPropertyDescriptor(getDelegateValue(), desc));
        }
      }
      else
      {
        propertyDescriptors = Collections.emptyList();
      }
    }
    return propertyDescriptors;
  }

  /**
   * Uses the delegate item provider to return an editable value.
   */
  @Override
  public Object getEditableValue(Object object)
  {
    return delegateItemProvider instanceof IItemPropertySource ?
      ((IItemPropertySource)delegateItemProvider).getEditableValue(getDelegateValue()) :
      null;
  }
  
  /**
   * Uses the delegate item provider to return the delegate value's new child descriptors.
   */
  @Override
  public Collection<?> getNewChildDescriptors(Object object, EditingDomain editingDomain, Object sibling)
  {
    return delegateItemProvider instanceof IEditingDomainItemProvider ?
      ((IEditingDomainItemProvider)delegateItemProvider).getNewChildDescriptors(getDelegateValue(), editingDomain, sibling) :
      Collections.emptyList();
  }

  /**
   * Uses the delegate item provider to create a command for the delegate value, and then calls {@link #wrapCommand
   * wrapCommand} to return an appropriate wrapper-substituting command wrapper for it. Drag and drop commands are
   * created directly by calling {@link WrapperItemProvider#createDragAndDropCommand createDragAndDropCommand}.
   */
  @Override
  public Command createCommand(Object object, EditingDomain domain, Class<? extends Command> commandClass, CommandParameter commandParameter)
  {
    if (commandClass == DragAndDropCommand.class)
    {
      DragAndDropCommand.Detail detail = (DragAndDropCommand.Detail)commandParameter.getFeature();
      return createDragAndDropCommand(domain, commandParameter.getOwner(), detail.location, detail.operations, detail.operation, commandParameter.getCollection());
    }
    
    if (delegateItemProvider instanceof IEditingDomainItemProvider)
    {
      Object commandOwner = getDelegateValue();
      Command result = null;

      // A SetCommand needs to go through SetCommand.create() to ensure it can execute and undo.
      //
      if (commandClass == SetCommand.class)
      {
        Object feature = commandParameter.getFeature();
        result = SetCommand.create(domain, commandOwner, feature, commandParameter.getValue(), commandParameter.getIndex());

        // A set command without a feature sets the value of this wrapper, hence replacing it with a new wrapper. So,
        // we need a special command wrapper that selects this new wrapper as the affected object.
        //
        if (feature == null)
        {
          return new ReplacementAffectedObjectCommand(result);          
        }
      }
      else
      {
        commandParameter.setOwner(commandOwner);
        result = ((IEditingDomainItemProvider)delegateItemProvider).createCommand(commandOwner, domain, commandClass, commandParameter);        
      }
      return wrapCommand(result, commandClass);
    }
    return UnexecutableCommand.INSTANCE;
  }

  /**
   * Wraps the given command in an appropriate command that will substitute the delegating wrapper for its value and
   * child wrappers for their corresponding values, whenever they appear in the affected objects. This implementation
   * returns an {@link AffectedObjectsWrappingCommand} or an {@link AffectedObjectsWrappingCommandActionDelegate},
   * depending on whether the given command implements {@link CommandActionDelegate}.
   */
  protected Command wrapCommand(Command command, Class<? extends Command> commandClass)
  {
    return command instanceof CommandActionDelegate ?
      new AffectedObjectsWrappingCommandActionDelegate((CommandActionDelegate)command) :
      new AffectedObjectsWrappingCommand(command);    
  }

  /**
   * An <code>AffectedObjectsWrappingCommand</code> wraps another command to substitute this wrapper for its value
   * and child wrappers for their corresponding child values, whenever they appear in the affected objects.
   */
  protected class AffectedObjectsWrappingCommand extends CommandWrapper
  {
    public AffectedObjectsWrappingCommand(Command command)
    {
      super(command);
    }

    @Override
    public Collection<?> getAffectedObjects()
    {
      List<Object> result = new ArrayList<Object>(super.getAffectedObjects());
      updateChildren();

      for (ListIterator<Object> i = result.listIterator(); i.hasNext(); )
      {
        Object object = i.next();

        if (object == getDelegateValue())
        {
          i.set(DelegatingWrapperItemProvider.this);
        }
        else if (childrenMap != null)
        {
          Object wrapper = childrenMap.get(object);
          if (wrapper != null)
          {
            i.set(wrapper);
          }
        }
      }
      return result;
    }
  }

  /**
   * An <code>AffectedObjectsWrappingCommandActionDelegate</code> wraps another command that also implements
   * <code>CommandActionDelegate</code>, to substitute this wrapper for its value and child wrappers for their
   * corresponding child values, whenever they appear in the affected objects. Action delegate methods are delegated
   * directly to the wrapped command.
   */
  protected class AffectedObjectsWrappingCommandActionDelegate extends AffectedObjectsWrappingCommand
    implements CommandActionDelegate
  {
    CommandActionDelegate commandActionDelegate;
    
    /**
     * Returns a new <code>AffectedObjectsWrappingCommandActionDelegate</code> for the given command.
     * @exception ClassCastException If the specified command does not implement {@link org.eclipse.emf.common.command.Command}.
     */
    public AffectedObjectsWrappingCommandActionDelegate(CommandActionDelegate command)
    {
      super((Command)command);
      commandActionDelegate = command;
    }

    @Override
    public boolean canExecute()
    {
      return commandActionDelegate.canExecute();
    }

    public Object getImage()
    {
      return commandActionDelegate.getImage();
    }

    public String getText()
    {
      return commandActionDelegate.getText();
    }

    @Override
    public String getDescription()
    {
      return commandActionDelegate.getDescription();
    }

    public String getToolTipText()
    {
      return commandActionDelegate.getToolTipText();
    }
  }

  /**
   * Adds a listener to receive this wrapper's repeated notifications.
   */
  public void addListener(INotifyChangedListener listener)
  {
    if (changeNotifier == null)
    {
      changeNotifier = new ChangeNotifier();
    }
    changeNotifier.addListener(listener);
  }

  /**
   * Removes a notification listener.
   */
  public void removeListener(INotifyChangedListener listener)
  {
    if (changeNotifier != null)
    {
      changeNotifier.removeListener(listener);
    }
  }

  /**
   * Fires a notification to the adapter factory and any registered listeners.
   */
  public void fireNotifyChanged(Notification notification)
  {
    if (adapterFactory instanceof IChangeNotifier)
    {
      IChangeNotifier adapterFactoryChangeNotifier = (IChangeNotifier)adapterFactory;
      adapterFactoryChangeNotifier.fireNotifyChanged(notification);
    }
    if (changeNotifier != null)
    {
      changeNotifier.fireNotifyChanged(notification);
    }
  }

  /**
   * Called by {@link #delegateItemProvider} when it normally fires a notification to it's adapter factory; if the
   * notification originated from the delegate value, this repeats the notification, using {@link #wrapNotification
   * wrapNotification} to substitute this wrapper as the operative object.
   */
  public void notifyChanged(Notification notification)
  {
    if (getRefreshElement(notification) == getDelegateValue())
    {
      fireNotifyChanged(wrapNotification(notification));
    }
  }

  /**
   * Returns the operative object of this notification, from which the viewer would be refreshed. If the notification
   * is an {@link IViewerNotification}, the {@link IViewerNotification#getElement element} is returned. Otherwise, the
   * {@link org.eclipse.emf.common.notify.Notification#getNotifier notifier} is returned.
   */
  protected Object getRefreshElement(Notification notification)
  {
    if (notification instanceof IViewerNotification)
    {
      return ((IViewerNotification)notification).getElement();
    }
    return notification.getNotifier();
  }

  /**
   * Wraps the given notification, substituting this wrapper as the operative object, by calling {@link
   * ViewerNotification#wrapNotification ViewerNotification.wrapNotification}.
   */
  protected Notification wrapNotification(Notification notification)
  {
    return ViewerNotification.wrapNotification(notification, this);
  }

  /**
   * A <code>DelegatingWrapperItemPropertyDescriptor</code> decorates an <code>ItemPropertyDescriptor</code> and
   * manages a command owner override. If its command owner is non-null, it ensures that the decorated descriptor,
   * if it also implements <code>OverrideableCommandOwner</code>, will have its command owner set to the same object
   * when {@link #resetPropertyValue resetPropertyValue} or {@link #setPropertyValue setPropertyValue} is called.
   * If its command owner is null, then the decorated descriptors's command owner will be set to this wrapper item
   * provider.
   */
  protected class DelegatingWrapperItemPropertyDescriptor extends ItemPropertyDescriptorDecorator
    implements OverrideableCommandOwner
  {
    protected Object commandOwner;
    
    public DelegatingWrapperItemPropertyDescriptor(Object object, IItemPropertyDescriptor itemPropertyDescriptor)
    {
      super(object, itemPropertyDescriptor);
    }

    /**
     * Sets the override command owner and, if the decorated descriptor also implements {@link
     * IItemPropertyDescriptor.OverrideableCommandOwner OverrideableCommandOwner}, updates its command owner.
     */
    public void setCommandOwner(Object commandOwner)
    {
      this.commandOwner = commandOwner;
      if (itemPropertyDescriptor instanceof OverrideableCommandOwner)
      {
        ((OverrideableCommandOwner)itemPropertyDescriptor).setCommandOwner(commandOwner);
      }
    }

    /**
     * Returns the override command owner.
     */
    public Object getCommandOwner()
    {
      return commandOwner;
    }

    /**
     * Updates the decorated descriptor's command owner and invokes <code>resetPropertyValue</code> on it.
     */
    @Override
    public void resetPropertyValue(Object thisObject)
    {
      boolean hasCommandOwner = commandOwner != null;
      if (!hasCommandOwner)
      {
        setCommandOwner(DelegatingWrapperItemProvider.this);
      }
      itemPropertyDescriptor.resetPropertyValue(object);
      if (!hasCommandOwner)
      {
        setCommandOwner(null);
      }
    }

    /**
     * Updates the decorated descriptor's command owner and invokes <code>setPropertyValue</code> on it.

     */
    @Override
    public void setPropertyValue(Object thisObject, Object value)
    {
      boolean hasCommandOwner = commandOwner != null;
      if (!hasCommandOwner)
      {
        setCommandOwner(DelegatingWrapperItemProvider.this);
      }
      itemPropertyDescriptor.setPropertyValue(object, value);
      if (!hasCommandOwner)
      {
        setCommandOwner(null);
      }
    }
  }
}