/**
 * Copyright (c) 2002-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.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.EMap;
import org.eclipse.emf.common.util.URI;
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.EModelElement;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
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.util.BasicExtendedMetaData;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.ExtendedMetaData;
import org.eclipse.emf.ecore.util.FeatureMap;
import org.eclipse.emf.ecore.util.FeatureMapUtil;
import org.eclipse.emf.ecore.xml.type.XMLTypePackage;


/**
 * This adapter implementation provides reflective support 
 * that emulates the behaviour of a default generated item provider.
 */
public class ReflectiveItemProvider
  extends ItemProviderAdapter
  implements
    IEditingDomainItemProvider,
    IStructuredItemContentProvider,
    ITreeItemContentProvider,
    IItemLabelProvider,
    IItemPropertySource
{
  /**
   */
  public ReflectiveItemProvider(AdapterFactory adapterFactory)
  {
    super(adapterFactory);
  }

  /**
   */
  @Override
  public List<IItemPropertyDescriptor> getPropertyDescriptors(Object object) 
  {
    // if (itemPropertyDescriptors == null)
    {
      itemPropertyDescriptors = new ArrayList<IItemPropertyDescriptor>();

      for (EStructuralFeature eFeature : ((EObject)object).eClass().getEAllStructuralFeatures())
      {
        if (!(eFeature instanceof EReference) || !((EReference)eFeature).isContainment())
        {
          String propertyEditorFactory = EcoreUtil.getAnnotation(eFeature, EcoreUtil.GEN_MODEL_ANNOTATION_URI, "propertyEditorFactory");
          if (propertyEditorFactory == null)
          {
            propertyEditorFactory = EcoreUtil.getAnnotation(eFeature.getEType(), EcoreUtil.GEN_MODEL_ANNOTATION_URI, "propertyEditorFactory");
          }
          itemPropertyDescriptors.add
            (new ItemPropertyDescriptor
              (((ComposeableAdapterFactory)adapterFactory).getRootAdapterFactory(),
               getResourceLocator(),
               getFeatureText(eFeature),
               getResourceLocator().getString
                 ("_UI_Property_description", new Object [] { getFeatureText(eFeature), eFeature.getEType().getName() }),
               eFeature,
               eFeature.isChangeable(),
               false,
               false,
               ItemPropertyDescriptor.GENERIC_VALUE_IMAGE,
               null,
               null,
               propertyEditorFactory == null ? null : URI.createURI(propertyEditorFactory)));
        }
      }
    }
    return itemPropertyDescriptors;
  }

  protected List<EObject> allRoots;
  protected List<EPackage> allEPackages;
  protected List<EClass> allEClasses;

  protected void gatherAllMetaData(EObject eObject)
  {
    EObject container = eObject.eContainer();
    if (container == null)
    {
      Resource resource = eObject.eResource();
      ResourceSet resourceSet = resource == null ? null : resource.getResourceSet();
      ExtendedMetaData extendedMetaData =
          resourceSet == null ?
            ExtendedMetaData.INSTANCE :
            new BasicExtendedMetaData(eObject.eResource().getResourceSet().getPackageRegistry());
      EStructuralFeature xmlnsPrefixMapFeature = extendedMetaData.getXMLNSPrefixMapFeature(eObject.eClass());
      if (xmlnsPrefixMapFeature != null)
      {
        @SuppressWarnings("unchecked")
        EMap<String, String> map = ((EMap<String, String>)eObject.eGet(xmlnsPrefixMapFeature));
        for (Map.Entry<String, String> entry : map)
        {
          EPackage ePackage = extendedMetaData.getPackage(entry.getValue());
          if (ePackage != null)
          {
            gatherRootContainerMetaData(ePackage);
          }
        }
      }

      if (resourceSet != null)
      {
        EPackage.Registry packageRegistry = resourceSet.getPackageRegistry();
        for (Object ePackageValue : packageRegistry.values())
        {
          if (ePackageValue instanceof EPackage)
          {
            gatherRootContainerMetaData((EPackage)ePackageValue);
          }
        }

        EList<Resource> resources = resourceSet.getResources();
        for (int i = 0; i  < resources.size(); ++i)
        {
          Resource otherResource = resources.get(i);
          if (otherResource != resource)
          {
            for (EObject otherEObject : otherResource.getContents())
            {
              if (otherEObject instanceof EPackage)
              {
                gatherMetaData((EPackage)otherEObject);
              }
            }
          }
        }
      }
    }
    else
    {
      gatherAllMetaData(container);
    }
    
    gatherMetaData(eObject.eClass());
  }

  private void gatherRootContainerMetaData(EPackage ePackage)
  {
    EObject rootContainer = EcoreUtil.getRootContainer(ePackage);
    if (rootContainer instanceof EModelElement)
    {
      gatherMetaData((EModelElement) rootContainer);
    }
    else
    {
      // The package may be rooted by some other arbitrary model, so look up the containers until we reach something that's not a model element.
      //
      EModelElement bestContainer = ePackage;
      for (EObject eContainer = bestContainer.eContainer(); eContainer instanceof EModelElement; eContainer = eContainer.eContainer())
      {
        bestContainer = (EModelElement) eContainer;
      }
      gatherMetaData(bestContainer);
    }
  }

  protected List<EClass> getAllEClasses(EClass eClass)
  {
    gatherMetaData(eClass);
    return allEClasses;
  }
  
  protected List<EPackage> getAllEPackages(EClass eClass)
  {
    gatherMetaData(eClass);
    return allEPackages;
  }
    
  protected void gatherMetaData(EModelElement eModelElement)
  {
    if (allRoots == null)
    {
      allRoots = new ArrayList<EObject>();
      allEClasses = new ArrayList<EClass>();
      allEPackages = new ArrayList<EPackage>();
    }
    
    Set<EObject> roots = new HashSet<EObject>();
    EObject root = EcoreUtil.getRootContainer(eModelElement);
    for (;;)
    {
      if (!allRoots.contains(root))
      {
        allRoots.add(root);
        ExtendedMetaData extendedMetaData = 
            root.eResource() == null || root.eResource().getResourceSet() == null ?
              ExtendedMetaData.INSTANCE :
              new BasicExtendedMetaData(root.eResource().getResourceSet().getPackageRegistry());
        if (root instanceof EPackage)
        {
          allEPackages.add((EPackage)root);
        }
        for (Iterator<EObject> i = root.eAllContents(); i.hasNext(); )
        {
          EObject eObject = i.next();
          
          if (eObject instanceof EClassifier)
          {
            extendedMetaData.getName((EClassifier)eObject);
            if (eObject instanceof EClass)
            {
              allEClasses.add((EClass)eObject);
            }
          }
          else if (eObject instanceof EStructuralFeature)
          {
            extendedMetaData.getName((EStructuralFeature)eObject);
          }
          else if (eObject instanceof EPackage)
          {
            allEPackages.add((EPackage)eObject);
          }
          
          for (EReference eReference : eObject.eClass().getEAllReferences())
          {
            if (!eReference.isDerived() && !eReference.isContainer() && !eReference.isContainment())
            {
              if (eReference.isMany())
              {
                @SuppressWarnings("unchecked")
                List<EObject> eObjects = ((List<EObject>)eObject.eGet(eReference));
                for (EObject crossReference : eObjects)
                {
                  if (crossReference != null)
                  {
                    EObject otherRoot = EcoreUtil.getRootContainer(crossReference);
                    if (!allRoots.contains(otherRoot))
                    {
                      roots.add(otherRoot);
                    }
                  } 
                }
              }
              else
              {
                EObject crossReference = (EObject)eObject.eGet(eReference);
                if (crossReference != null)
                {
                  EObject otherRoot = EcoreUtil.getRootContainer(crossReference);
                  if (!allRoots.contains(otherRoot))
                  {
                    roots.add(otherRoot);
                  }
                }
              }
            }
          }
        }
      }

      if (roots.isEmpty())
      {
        break;
      }
      else
      {
        Iterator<EObject> i = roots.iterator();
        root = i.next();
        i.remove();
      }
    }
  }

  protected List<EClass> getAllChildConcreteSubclasses(EReference eReference)
  {
    EClass eClass = eReference.getEReferenceType();
    List<EClass> result = getAllConcreteSubclasses(eClass);
    if (eClass == EcorePackage.Literals.EOBJECT && eReference.getEAnnotation(ExtendedMetaData.ANNOTATION_URI) != null)
    {
      if (!result.contains(XMLTypePackage.Literals.ANY_TYPE))
      {
        result.add(XMLTypePackage.Literals.ANY_TYPE);
      }
    }
    return result;
  }

  protected List<EClass> getAllConcreteSubclasses(EClass eClass)
  {
    List<EClass> result = new ArrayList<EClass>();
    if (eClass == EcorePackage.Literals.EOBJECT)
    {
      for (EClass otherEClass : getAllEClasses(eClass))
      {
        if (!otherEClass.isAbstract() && !ExtendedMetaData.INSTANCE.isAnonymous(otherEClass))
        {
          result.add(otherEClass);
        }
      }
    }
    else if (ExtendedMetaData.INSTANCE.isAnonymous(eClass))
    {
      result.add(eClass);
    }
    else
    {
      for (EClass otherEClass : getAllEClasses(eClass))
      {
        if (!otherEClass.isAbstract() && eClass.isSuperTypeOf(otherEClass) && !ExtendedMetaData.INSTANCE.isAnonymous(otherEClass))
        {
          result.add(otherEClass);
        }
      }
    }
    return  result;
  }

  /**
   */
  @Override
  protected Collection<? extends EStructuralFeature> getChildrenFeatures(Object object)
  {
    // if (childrenFeatures == null)
    {
      childrenFeatures = new ArrayList<EStructuralFeature>();
      EObject eObject = (EObject)object;
      EClass eClass = eObject.eClass();
      if (ExtendedMetaData.INSTANCE.getContentKind(eClass) != ExtendedMetaData.MIXED_CONTENT)
      {
        for (EReference eReference : eClass.getEAllReferences())
        {
          if (eReference.isContainment() && ExtendedMetaData.INSTANCE.getGroup(eReference) == null)
          {
            childrenFeatures.add(eReference);
          }
        }
      }
      for (EAttribute eAttribute : eClass.getEAllAttributes())
      {
        if (ExtendedMetaData.INSTANCE.getGroup(eAttribute) == null && 
              eAttribute.getEType().getInstanceClass() == FeatureMap.Entry.class &&
              !eAttribute.isDerived())
        {
          childrenFeatures.add(eAttribute);
        }
      }
    }
    return childrenFeatures;
  }

  /**
   */
  @Override
  public Object getImage(Object object)
  {
    EObject eObject = (EObject)object;
    EClass eClass = eObject.eClass();
    return URI.createURI(getResourceLocator().getImage("full/obj16/Item").toString() + "#" + eClass.getName());
  }

  /**
   */
  @Override
  public String getText(Object object)
  {
    EObject eObject = (EObject)object;
    EClass eClass = eObject.eClass();
    String label = format(capName(eClass.getName()), ' ');

    EStructuralFeature feature = getLabelFeature(eClass);
    if (feature != null)
    {
      Object value = eObject.eGet(feature);
      if (value != null)
      {
        return label + " " + value.toString();
      }
    }
    return label;
  }

  /**
   */  
  protected EStructuralFeature getLabelFeature(EClass eClass)
  {
    EAttribute result = null;
    for (EAttribute eAttribute : eClass.getEAllAttributes())
    {
      if (!eAttribute.isMany() && eAttribute.getEType().getInstanceClass() != FeatureMap.Entry.class)
      {
        if ("name".equalsIgnoreCase(eAttribute.getName()))
        {
          result = eAttribute;
          break;
        }
        else if (result == null)
        {
          result = eAttribute;
        }
        else if (eAttribute.getEAttributeType().getInstanceClass() == String.class && 
                 result.getEAttributeType().getInstanceClass() != String.class)
        {
          result = eAttribute;
        }
      }
    }
    return result;
  }

  /**
   */
  protected String capName(String name)
  {
    return name.length() == 0 ? name : name.substring(0, 1).toUpperCase() + name.substring(1);
  }

  /**
   */
  public String format(String name, char separator)
  {
    StringBuilder result = new StringBuilder();

    for (Iterator<String> i = parseName(name, '_').iterator(); i.hasNext(); )
    {
      String component = i.next();
      result.append(component);
      if (i.hasNext() && component.length() > 1)
      {
        result.append(separator);
      }
    }
    return result.toString();
  }

  /**
   */
  protected List<String> parseName(String sourceName, char sourceSeparator)
  {
    List<String> result = new ArrayList<String>();
    StringBuffer currentWord = new StringBuffer();

    int length = sourceName.length();
    boolean lastIsLower = false;

    for (int index = 0; index < length; index++)
    {
      char curChar = sourceName.charAt(index);
      if (Character.isUpperCase(curChar) || (!lastIsLower && Character.isDigit(curChar)) || curChar == sourceSeparator)
      {
        if (lastIsLower || curChar == sourceSeparator)
        {
          result.add(currentWord.toString());
          currentWord = new StringBuffer();
        }
        lastIsLower = false;
      }
      else
      {
        if (!lastIsLower)
        {
          int currentWordLength = currentWord.length();
          if (currentWordLength > 1)
          {
            char lastChar = currentWord.charAt(--currentWordLength);
            currentWord.setLength(currentWordLength);
            result.add(currentWord.toString());
            currentWord = new StringBuffer();
            currentWord.append(lastChar);
          }
        }
        lastIsLower = true;
      }
      if (curChar != sourceSeparator)
      {
        currentWord.append(curChar);
      }
    }

    result.add(currentWord.toString());
    return result;
  }

  /**
   */  
  protected List<? extends EStructuralFeature> getAllDelegatedFeatures(EStructuralFeature feature)
  {
    if (!FeatureMapUtil.isFeatureMap(feature)) return Collections.emptyList();

    EClass eClass = feature.getEContainingClass();
    List<EStructuralFeature> delegated = new ArrayList<EStructuralFeature>();

    if (ExtendedMetaData.INSTANCE.getMixedFeature(eClass) == feature)
    {
       delegated.add(XMLTypePackage.Literals.XML_TYPE_DOCUMENT_ROOT__COMMENT);
       delegated.add(XMLTypePackage.Literals.XML_TYPE_DOCUMENT_ROOT__TEXT);
       delegated.add(XMLTypePackage.Literals.XML_TYPE_DOCUMENT_ROOT__PROCESSING_INSTRUCTION);

       if (ExtendedMetaData.INSTANCE.getDocumentRoot(eClass.getEPackage()) != eClass)
       {
         delegated.add(XMLTypePackage.Literals.XML_TYPE_DOCUMENT_ROOT__CDATA);
       }

       for (EStructuralFeature otherFeature : eClass.getEAllStructuralFeatures())
       {
         if (otherFeature != feature && otherFeature.isDerived() && otherFeature.isChangeable() &&
             ExtendedMetaData.INSTANCE.getGroup(otherFeature) == null)
         {
           delegated.add(otherFeature);
         }
       }
    }
    else
    {
      switch (ExtendedMetaData.INSTANCE.getFeatureKind(feature))
      {
        case ExtendedMetaData.GROUP_FEATURE:
        {
          Set<EStructuralFeature> allDelegated = new HashSet<EStructuralFeature>();
          Set<String> qNames = new HashSet<String>();
          for (EStructuralFeature otherFeature : eClass.getEStructuralFeatures())
          {
            if (otherFeature != feature && otherFeature.isDerived() && 
                ExtendedMetaData.INSTANCE.getGroup(otherFeature) == feature)
            { 
              if (otherFeature.isChangeable())
              {
                delegated.add(otherFeature);
                qNames.add
                  (ExtendedMetaData.INSTANCE.getNamespace(otherFeature) + "#" + 
                     ExtendedMetaData.INSTANCE.getName(otherFeature));
              }
              allDelegated.add(otherFeature);
            }
          }
          
          for (EPackage ePackage : getAllEPackages(eClass))
          {
            EClass documentRootEClass = ExtendedMetaData.INSTANCE.getDocumentRoot(ePackage);
            if (documentRootEClass != null)
            {
              for (EStructuralFeature otherFeature : documentRootEClass.getEAllStructuralFeatures())
              {
                if (otherFeature != feature && 
                      otherFeature.isChangeable() &&
                      otherFeature.isDerived() &&
                      allDelegated.contains(ExtendedMetaData.INSTANCE.getAffiliation(eClass, otherFeature))  &&
                      qNames.add
                        (ExtendedMetaData.INSTANCE.getNamespace(otherFeature) + "#" + 
                           ExtendedMetaData.INSTANCE.getName(otherFeature)))
                {
                  delegated.add(otherFeature);
                }
              }
            }
          }
            
          break;
        }
        case ExtendedMetaData.ATTRIBUTE_WILDCARD_FEATURE:
        case ExtendedMetaData.ELEMENT_WILDCARD_FEATURE:
        {
          for (EPackage ePackage : getAllEPackages(eClass))
          {
            EClass documentRootEClass = ExtendedMetaData.INSTANCE.getDocumentRoot(ePackage);
            if (documentRootEClass != null)
            {
              for (EStructuralFeature otherFeature : documentRootEClass.getEAllStructuralFeatures())
              {
                if (otherFeature != feature && 
                      otherFeature.isDerived() && 
                      otherFeature.isChangeable() &&
                      ExtendedMetaData.INSTANCE.getAffiliation(eClass, otherFeature) == feature)
                {
                  delegated.add(otherFeature);
                }
              }
            }
          }
          break;
        }
      }
    }

    List<EStructuralFeature> result = new ArrayList<EStructuralFeature>();
    for (EStructuralFeature delegatedFeature : delegated)
    {
      if (FeatureMapUtil.isFeatureMap(delegatedFeature))
      {
        result.addAll(getAllDelegatedFeatures(delegatedFeature));
      }
      else
      {
        result.add(delegatedFeature);
      }
    }
    return result;
  }

  /**
   */
  @Override
  protected void collectNewChildDescriptors(Collection<Object> newChildDescriptors, Object object)    
  {
    // This ensure that this package itself is traversed even if the reference type is EObject...
    //
    gatherAllMetaData((EObject)object);

    for (EStructuralFeature feature : getChildrenFeatures(object))
    {
      if (FeatureMapUtil.isFeatureMap(feature))
      {
        for (EStructuralFeature delegatedFeature : getAllDelegatedFeatures(feature))
        {
          if (delegatedFeature instanceof EAttribute)
          {
            EDataType type = ((EAttribute)delegatedFeature).getEAttributeType();
            Object value = delegatedFeature.getDefaultValue();

            if (value == null)
            {
              Class<?> instanceClass = type.getInstanceClass();

              if (instanceClass == String.class)
              {
                value = "";
              }
              else if (instanceClass == Boolean.class)
              {
                value = Boolean.FALSE;
              }
              else if (instanceClass == Character.class)
              {
                value = '\u0000';
              }
              else
              {
                String literal = instanceClass != null && Number.class.isAssignableFrom(instanceClass) ? "0" : ""; 
                try
                {
                  value = EcoreUtil.createFromString(type, literal);
                }
                catch (Exception e) 
                {
                  // Ignore
                }
              }
            }

            if (value != null)
            {
              newChildDescriptors.add(createChildParameter(feature, FeatureMapUtil.createEntry(delegatedFeature, value)));
            }
          }
          else if (delegatedFeature instanceof EReference)
          {
            for (EClass eClass : getAllChildConcreteSubclasses((EReference)delegatedFeature))
            {
              FeatureMap.Entry entry = FeatureMapUtil.createEntry(delegatedFeature, EcoreUtil.create(eClass));
              newChildDescriptors.add(createChildParameter(feature, entry));
            }
          }
        }
      }
      else if (feature instanceof EReference && feature.isChangeable())
      {
        for (EClass eClass : getAllChildConcreteSubclasses((EReference)feature))
        {
          newChildDescriptors.add(createChildParameter(feature, EcoreUtil.create(eClass)));
        }
      }
    }
  }

  @Override
  public String getCreateChildText(Object owner, Object feature, Object child, Collection<?> selection)
  {
    if (feature instanceof EStructuralFeature && FeatureMapUtil.isFeatureMap((EStructuralFeature)feature))
    {
      FeatureMap.Entry entry = (FeatureMap.Entry)child;
      feature = entry.getEStructuralFeature();
      child = entry.getValue();
    }

    String childType = feature instanceof EAttribute ? getTypeText((EAttribute)feature) : getTypeText(child);

    return 
      getResourceLocator().getString
        ("_UI_CreateChild_text2",
         new Object[] { childType, getFeatureText(feature), getTypeText(owner) });
  }

  /**
   */
  @Override
  public Object getCreateChildImage(Object owner, Object feature, Object child, Collection<?> selection)
  {
    if (feature instanceof EStructuralFeature && FeatureMapUtil.isFeatureMap((EStructuralFeature)feature))
    {
      FeatureMap.Entry entry = (FeatureMap.Entry)child;
      feature = entry.getEStructuralFeature();
      child = entry.getValue();        
    }

    if (feature instanceof EReference && child instanceof EObject)
    {
      EReference reference = (EReference)feature;
      EClass parentClass = reference.getEContainingClass();
      EClass childClass = ((EObject)child).eClass();
      return  
        URI.createURI
          (getResourceLocator().getImage("full/ctool16/CreateChild").toString() + 
             "#" + parentClass.getName() + "/" + childClass.getName());
    }

    return getResourceLocator().getImage("full/ctool16/CreateChild");
  }

  /**
   */
  @Override
  protected String getTypeText(Object object)
  {
    String text = object instanceof EObject ? ((EObject)object).eClass().getName() : getString("_UI_Unknown_type");
    return format(capName(text), ' ');
  }

  /**
   */
  @Override
  protected String getTypeText(EAttribute attribute)
  {
    return format(capName(attribute.getEAttributeType().getName()), ' ');
  }

  /**
   */
  @Override
  protected String getFeatureText(Object feature)
  {
    String text = feature instanceof EStructuralFeature ?
      ((EStructuralFeature)feature).getName() : getResourceLocator().getString("_UI_Unknown_feature");
    return format(capName(text), ' ');
  }

  /**
   */
  @Override
  public void notifyChanged(Notification notification)
  {
    updateChildren(notification);

    EObject object = (EObject)notification.getNotifier();
    EClass eClass = object.eClass();
    EStructuralFeature feature = (EStructuralFeature)notification.getFeature();

    // Is this a containment reference child?
    //
    boolean child = 
      ExtendedMetaData.INSTANCE.getContentKind(eClass) != ExtendedMetaData.MIXED_CONTENT &&
      feature instanceof EReference && ((EReference)feature).isContainment() &&
      ExtendedMetaData.INSTANCE.getGroup(feature) == null;

    // Or a feature map child?
    //
    child |=
      feature instanceof EAttribute &&
      ExtendedMetaData.INSTANCE.getGroup(feature) == null &&
      feature.getEType().getInstanceClass() == FeatureMap.Entry.class &&
      !feature.isDerived();

    // Is this the label feature?
    //
    boolean label = feature == getLabelFeature(eClass);

    fireNotifyChanged(new ViewerNotification(notification, object, child, !child || (child && label)));
  }
  
  @Override
  protected boolean isWrappingNeeded(Object object)
  {
    wrappingNeeded = null;
    return super.isWrappingNeeded(object);
  }
}
