/**
 * Copyright (c) 2011-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.resource;


import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.emf.codegen.ecore.genmodel.GenBase;
import org.eclipse.emf.codegen.ecore.genmodel.GenModel;
import org.eclipse.emf.codegen.ecore.genmodel.GenPackage;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EcorePackage;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.xcore.XDataType;
import org.eclipse.emf.ecore.xcore.XModelElement;
import org.eclipse.emf.ecore.xcore.XNamedElement;
import org.eclipse.emf.ecore.xcore.XOperation;
import org.eclipse.emf.ecore.xcore.XPackage;
import org.eclipse.emf.ecore.xcore.XStructuralFeature;
import org.eclipse.emf.ecore.xcore.XcorePackage;
import org.eclipse.emf.ecore.xcore.mappings.ToXcoreMapping;
import org.eclipse.emf.ecore.xcore.mappings.XDataTypeMapping;
import org.eclipse.emf.ecore.xcore.mappings.XFeatureMapping;
import org.eclipse.emf.ecore.xcore.mappings.XcoreMapper;
import org.eclipse.emf.ecore.xcore.util.XcoreEcoreBuilder;
import org.eclipse.emf.ecore.xcore.util.XcoreGenModelBuilder;
import org.eclipse.emf.ecore.xcore.util.XcoreGenModelInitializer;
import org.eclipse.emf.ecore.xcore.util.XcoreJvmInferrer;
import org.eclipse.xtext.common.types.JvmIdentifiableElement;
import org.eclipse.xtext.common.types.TypesPackage;
import org.eclipse.xtext.parser.antlr.IReferableElementsUnloader;
import org.eclipse.xtext.resource.DerivedStateAwareResource;
import org.eclipse.xtext.resource.IDerivedStateComputer;
import org.eclipse.xtext.util.OnChangeEvictingCache;
import org.eclipse.xtext.util.concurrent.IUnitOfWork;
import org.eclipse.xtext.xbase.XBlockExpression;
import org.eclipse.xtext.xbase.XExpression;
import org.eclipse.xtext.xbase.jvmmodel.IJvmModelAssociations;
import org.eclipse.xtext.xbase.jvmmodel.ILogicalContainerProvider;

import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

@Singleton
public class XcoreModelAssociator implements IJvmModelAssociations, ILogicalContainerProvider, IDerivedStateComputer
{
  @Inject
  private OnChangeEvictingCache cache;

  @Inject
  protected XcoreJvmInferrer jvmInferrer;

  @Inject
  protected XcoreGenModelBuilder genModelBuilder;

  @Inject
  private Provider<XcoreEcoreBuilder> xcoreEcoreBuilderProvider;

  @Inject
  protected XcoreMapper mapper;

  @Inject
  private IReferableElementsUnloader unloader;

  @Inject
  protected XcoreGenModelInitializer genModelInitializer;

  public void installDerivedState(DerivedStateAwareResource resource, boolean preLinkingPhase)
  {
    if (resource.getParseResult() != null && resource.getParseResult().getRootASTElement() instanceof XPackage)
    {
      XPackage model = (XPackage)resource.getParseResult().getRootASTElement();
      final XcoreEcoreBuilder xcoreEcoreBuilder = xcoreEcoreBuilderProvider.get();
      EPackage ePackage = xcoreEcoreBuilder.getEPackage(model);
      resource.getContents().add(ePackage);
      final GenModel genModel = genModelBuilder.getGenModel(model);
      genModel.setCanGenerate(true);
      Collection<? extends Runnable> runnables = genModelInitializer.initialize(genModel, true);
      resource.getContents().addAll(jvmInferrer.inferElements(genModel));
      if (!preLinkingPhase)
      {
        cache.execWithoutCacheClear(resource, new IUnitOfWork.Void<Resource>()
        {
          @Override
          public void process(Resource state) throws Exception
          {
            xcoreEcoreBuilder.link();
          }
        });
        genModelBuilder.initializeUsedGenPackages(genModel);

        for (Runnable runnable : runnables)
        {
          runnable.run();
        }

        // If the model has edit support, it's important to determine if we have a dependencies on Ecore's generated item providers...
        //
        if (genModel.hasEditSupport())
        {
          for (GenPackage genPackage : genModel.getUsedGenPackages())
          {
            // If we find a GenPackage for Ecore itself...
            //
            if (EcorePackage.eNS_URI.equals(genPackage.getNSURI()))
            {
              boolean needsEcoreEditSupport = false;
              EPackage ecorePackage = genPackage.getEcorePackage();

              // Consider all the class of the package...
              LOOP:
              for (EClassifier eClassifier : ePackage.getEClassifiers())
              {
                if (eClassifier instanceof EClass)
                {
                  EClass eClass = (EClass)eClassifier;

                  // If one of the super types is from the Ecore package and isn't EObject, we need Ecore edit support.
                  //
                  for (EClass eSuperType : eClass.getEAllSuperTypes())
                  {
                    if (eSuperType.getEPackage() == ecorePackage && !"EObject".equals(eSuperType.getName()))
                    {
                      needsEcoreEditSupport = true;
                      break LOOP;
                    }
                  }
                  // If one of the reference types is from the Ecore package and isn't EObject, we need Ecore edit support.
                  //
                  for (EReference eReference : eClass.getEAllReferences())
                  {
                    EClass eReferenceType = eReference.getEReferenceType();
                    if (eReferenceType != null && eReferenceType.getEPackage() == ecorePackage && !"EObject".equals(eReferenceType.getName()))
                    {
                      needsEcoreEditSupport = true;
                      break LOOP;
                    }
                  }
                }
              }
              // Modify the Ecore package's GenPackage's model to indicate whether Ecore provides edit support.
              // Do this without producing notifications to avoid Ecore Tools transactional editing domain complaining that there is a model modification without a write transaction.
              //
              GenModel ecoreGenModel = genPackage.getGenModel();
              ecoreGenModel.eSetDeliver(false);
              ecoreGenModel.setEditDirectory(needsEcoreEditSupport ? "/org.eclipse.emf.edit.ecore/src" : "");
              ecoreGenModel.eSetDeliver(true);
              break;
            }
          }
        }

        cache.execWithoutCacheClear(resource, new IUnitOfWork.Void<Resource>()
        {
          @Override
          public void process(Resource state) throws Exception
          {
            jvmInferrer.inferDeepStructure(genModel);
          }
        });
      }
      resource.getCache().clear(resource);
    }
  }

  public void discardDerivedState(DerivedStateAwareResource resource)
  {
    EList<EObject> contents = resource.getContents();
    int size = contents.size();
    if (size > 1)
    {
      List<EObject> toBeRemoved = Lists.newArrayList();
      for (Iterator<EObject> i = contents.iterator(); i.hasNext();)
      {
        EObject eObject = i.next();
        if (eObject instanceof XPackage)
        {
          mapper.unsetMapping((XPackage)eObject);
        }
        else
        {
          unloader.unloadRoot(eObject);
          toBeRemoved.add(eObject);
        }
      }
      contents.removeAll(toBeRemoved);
    }
  }

  public XExpression getAssociatedExpression(JvmIdentifiableElement element)
  {
    ToXcoreMapping mapping = mapper.getToXcoreMapping(element);
    XNamedElement xcoreElement = mapping.getXcoreElement();
    if (xcoreElement instanceof XOperation)
    {
      XOperation xOperation = (XOperation)xcoreElement;
      if (element == mapper.getMapping(xOperation).getJvmOperation())
      {
        return xOperation.getBody();
      }
    }
    else if (xcoreElement instanceof XDataType)
    {
      XDataType xDataType = (XDataType)xcoreElement;
      XDataTypeMapping typeMapping = mapper.getMapping(xDataType);
      if (element == typeMapping.getConverter())
      {
        return xDataType.getConvertBody();
      }
      else if (element == typeMapping.getCreator())
      {
        return xDataType.getCreateBody();
      }
    }
    else if (xcoreElement instanceof XStructuralFeature)
    {
      XStructuralFeature feature = (XStructuralFeature)xcoreElement;
      XFeatureMapping featureMapping = mapper.getMapping(feature);
      if (element == featureMapping.getGetter())
      {
        return feature.getGetBody();
      }
      else if (element == featureMapping.getSetter())
      {
        return feature.getSetBody();
      }
      else if (element == featureMapping.getIsSetter())
      {
        return feature.getIsSetBody();
      }
      else if (element == featureMapping.getUnsetter())
      {
        return feature.getUnsetBody();
      }
    }
    return null;
  }

  public Set<EObject> getJvmElements(EObject eObject)
  {
    final Set<EObject> result = Sets.newLinkedHashSet();
    if (eObject instanceof XNamedElement)
    {
      GenBase genBase = mapper.getGen((XNamedElement)eObject);
      if (genBase != null)
      {
        result.addAll(XcoreJvmInferrer.getInferredElements(genBase));
      }
    }
    else if (eObject instanceof GenBase)
    {
      result.addAll(XcoreJvmInferrer.getInferredElements((GenBase)eObject));
    }
    else if (eObject.eClass().getEPackage() == TypesPackage.eINSTANCE)
    {
      result.add(eObject);
    }
    return result;
  }

  public Set<EObject> getSourceElements(EObject eObject)
  {
    EObject xcoreElement = mapper.getXcoreElement(eObject);
    return xcoreElement == null || xcoreElement == eObject ? Collections.<EObject>emptySet() : Collections.singleton(eObject);
  }

  public EObject getPrimarySourceElement(EObject eObject)
  {
    EObject xcoreElement = mapper.getXcoreElement(eObject);
    return xcoreElement;
  }

  public JvmIdentifiableElement getLogicalContainer(XExpression xExpression)
  {
    if (xExpression instanceof XBlockExpression)
    {
      EObject eContainer = xExpression.eContainer();
      EReference eContainmentFeature = xExpression.eContainmentFeature();
      if (eContainmentFeature == XcorePackage.Literals.XOPERATION__BODY)
      {
        return mapper.getMapping((XOperation)eContainer).getJvmOperation();
      }
      else if (eContainmentFeature == XcorePackage.Literals.XDATA_TYPE__CREATE_BODY)
      {
        return mapper.getMapping((XDataType)eContainer).getCreator();
      }
      else if (eContainmentFeature == XcorePackage.Literals.XDATA_TYPE__CONVERT_BODY)
      {
        return mapper.getMapping((XDataType)eContainer).getConverter();
      }
      else if (eContainmentFeature == XcorePackage.Literals.XSTRUCTURAL_FEATURE__GET_BODY)
      {
        return mapper.getMapping((XStructuralFeature)eContainer).getGetter();
      }
      else if (eContainmentFeature == XcorePackage.Literals.XSTRUCTURAL_FEATURE__SET_BODY)
      {
        return mapper.getMapping((XStructuralFeature)eContainer).getSetter();
      }
      else if (eContainmentFeature == XcorePackage.Literals.XSTRUCTURAL_FEATURE__IS_SET_BODY)
      {
        return mapper.getMapping((XStructuralFeature)eContainer).getIsSetter();
      }
      else if (eContainmentFeature == XcorePackage.Literals.XSTRUCTURAL_FEATURE__UNSET_BODY)
      {
        return mapper.getMapping((XStructuralFeature)eContainer).getUnsetter();
      }
      else
      {
        return null;
      }
    }
    return null;
  }

  public JvmIdentifiableElement getNearestLogicalContainer(EObject eObject)
  {
    for (EObject eContainer = eObject; eContainer != null; eContainer = eContainer.eContainer())
    {
      if (eContainer instanceof XExpression && eContainer.eContainer() instanceof XModelElement)
      {
        return getLogicalContainer((XExpression)eContainer);
      }
    }
    return null;
  }

  public EObject getPrimaryJvmElement(EObject sourceElement)
  {
    return Iterables.getFirst(getJvmElements(sourceElement), null);
  }

  public boolean isPrimaryJvmElement(EObject jvmElement)
  {
    return getPrimaryJvmElement(getPrimarySourceElement(jvmElement)) == jvmElement;
  }
}
