package de.hub.emffrag.fragmentation;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

import org.eclipse.emf.common.notify.NotificationChain;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
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.EStructuralFeature.Setting;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.impl.DynamicEObjectImpl;
import org.eclipse.emf.ecore.impl.EClassImpl;
import org.eclipse.emf.ecore.impl.EStructuralFeatureImpl;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.Resource.Internal;
import org.eclipse.emf.ecore.util.EContentsEList;
import org.eclipse.emf.ecore.util.EcoreEList;
import org.eclipse.emf.ecore.util.InternalEList;

import de.hub.emffrag.EmfFragActivator;
import de.hub.emffrag.util.EMFFragUtil;
import de.hub.emffrag.util.EMFFragUtil.FragmentationType;

public class FInternalObjectImpl extends DynamicEObjectImpl implements FInternalObject {

	boolean hasPriliminaryId = false;
	boolean hasDefaultModelId = false;
	long indexBeforeAdd = -1;
	String addValueSetId = null;
	
	public FInternalObjectImpl(EClass eClass) {
		super(eClass);
		EmfFragActivator.instance.globalEventListener.onInternalObjectCreated(this);
		onLoad();
	}

	public boolean isFragmentRoot() {
		Resource eResource = eResource();
		return eResource != null && eResource instanceof Fragment
				&& (eContainer() == null || eResource != eContainer().eResource());
	}

	public EObject getUserObject() {
		return UserObjectsCache.instance.getUserObject(this);
	}

	public FragmentedModel getFragmentation() {
		Fragment fragment = getFragment();
		if (fragment != null) {
			return fragment.getFragmentedModel();
		} else {
			return null;
		}
	}

	public Fragment getFragment() {
		Resource eResource = eResource();
		if (eResource != null) {
			if (eResource instanceof Fragment) {
				return ((Fragment) eResource);
			} else {
				return null;
			}
		} else if (eIsProxy()) {
			EObject container = eContainer();
			while (container != null) {
				eResource = container.eResource();
				if (eResource != null) {
					if (eResource instanceof Fragment) {
						return ((Fragment) eResource);
					} else {
						return null;
					}
				}
				container = container.eContainer();
			}
			return null;
		} else {
			return null;
		}
	}
	
	private EList<EObject> eContentsWithOutFragments() {
		EStructuralFeature[] eStructuralFeatures = ((EClassImpl.FeatureSubsetSupplier) this.eClass().getEAllStructuralFeatures()).containments();
		EStructuralFeature[] eStructuralFeaturesWithOutFragmentsArray = null;
		if (eStructuralFeatures != null) {
			List<EStructuralFeature> eStructuralFeaturesWithOutFragments = new ArrayList<EStructuralFeature>(eStructuralFeatures.length);
			for (EStructuralFeature eStructuralFeature : eStructuralFeatures) {
				FragmentationType fragmentationType = EMFFragUtil.getFragmentationType(eStructuralFeature);
				if (fragmentationType == FragmentationType.None || eStructuralFeature instanceof EAttribute) {
					eStructuralFeaturesWithOutFragments.add(eStructuralFeature);
				}
			}
			eStructuralFeaturesWithOutFragmentsArray = eStructuralFeaturesWithOutFragments
					.toArray(new EStructuralFeature[eStructuralFeaturesWithOutFragments.size()]);
		}

		EContentsEList<EObject> eContentsEListWithOutFragments = eStructuralFeaturesWithOutFragmentsArray == null ? EContentsEList
				.<EObject> emptyContentsEList() : new EContentsEList<EObject>(this, eStructuralFeaturesWithOutFragmentsArray);

		return eContentsEListWithOutFragments;
	}

	@Override
	public NotificationChain eSetResource(Internal resource, NotificationChain notifications) {
		EmfFragActivator.instance.globalEventListener.onInternalObjectSetResource(this, resource);
		Resource oldResource = eResource();
		NotificationChain result = super.eSetResource(resource, notifications);
		updateAfterResourceChange(oldResource);
		return result;
	}

	private void updateAfterResourceChange(Resource oldResource) {
		EmfFragActivator.instance.idSemantics.onRequiredId(this);
		Fragment oldFragment = (Fragment) oldResource;
		Fragment newFragment = (Fragment) getFragment();
		if (oldFragment == newFragment) {
			return;
		}

		if (newFragment != null && oldFragment == null) {
			onCreate();
			EPackage metaModel = eClass().getEPackage();
			FragmentedModel fragmentedModel = newFragment.getFragmentedModel();
			if (fragmentedModel != null) {
				fragmentedModel.getInternalResourceSet().getPackageRegistry().put(metaModel.getNsURI(), metaModel);
			}
		}

		if (newFragment != null) {
			updateIdsAfterContainerChange(this, newFragment);
		} else {
			// TODO remove objects from the index?
		}
	}

	private void updateIdsAfterContainerChange(FInternalObjectImpl object, Fragment fragment) {
		EmfFragActivator.instance.idSemantics.onContainerChange(object, fragment.getFragmentedModel());	
		for (EObject content : object.eContentsWithOutFragments()) {
			updateIdsAfterContainerChange((FInternalObjectImpl) content, fragment);
		}
	}
	
	private void updateFragmentationAfterCreatingAFragment(FInternalObjectImpl object, Fragment newFragment) {
		EReference eContainmentFeature = object.eContainmentFeature();
		if (eContainmentFeature != null) {
			FragmentationType fragmentationType = EMFFragUtil.getFragmentationType(eContainmentFeature);
			boolean isFragmenting = fragmentationType == FragmentationType.FragmentsContainment || fragmentationType == FragmentationType.FragmentsIndexedContainment;
			if (isFragmenting) {
				if (!object.isFragmentRoot()) {
					object.updateAfterContainerChange(newFragment);
					if (newFragment != object.getFragment()) {
						return;
					}
				}
			}			
		}
		
		for (EObject content: object.eContents()) {
			if (((FInternalObjectImpl)content).getFragment() == newFragment) {
				updateFragmentationAfterCreatingAFragment((FInternalObjectImpl)content, newFragment);
			}
		}
	}

	private void updateAfterContainerChange(Fragment oldFragment) {
		Fragment currentFragment = getFragment();
		EStructuralFeature eContainingFeature = eContainingFeature();
		FragmentationType fragmentationType = eContainingFeature != null ? EMFFragUtil.getFragmentationType(eContainingFeature)
				: FragmentationType.None;
		boolean isFragmenting = fragmentationType == FragmentationType.FragmentsContainment
				|| fragmentationType == FragmentationType.FragmentsIndexedContainment;
		FragmentedModel fragmentation = getFragmentation();
		if (isFragmenting && fragmentation != null) {
			Fragment newFragment = null;
			if (fragmentURIForContainerChange != null) {				
				newFragment = fragmentation.createFragment(fragmentURIForContainerChange, this);
			} else if (!isFragmentRoot()) {
				newFragment = fragmentation.createFragment(null, this);
			}
			
			if (newFragment != null) {
				updateFragmentationAfterCreatingAFragment(this, newFragment);
			}
		}
		
		if (eContainer() == null && currentFragment != null) {
			// Object was removed (container set to null.
			// This will delete the resource (fragment) based on normal emf
			// semantics.
			currentFragment.getContents().remove(this);
		}

		if (oldFragment != eResource()) {
			updateAfterResourceChange(oldFragment);
			if (oldFragment != null) {
				// delete if the old fragment is empty and is not deleted
				// already
				if (oldFragment.getContents().isEmpty() && oldFragment.getResourceSet() != null) {
					oldFragment.getFragmentedModel().deleteFragment(oldFragment);
				}
			}
		} else {
			if (currentFragment != null) {
				updateIdsAfterContainerChange(this, currentFragment);
			}
		}

		fragmentURIForContainerChange = null;
	}

	/**
	 * The EMF-implementation does somehow create a network of Java references
	 * that prevent to fully unload the contents of a resource. This method
	 * breaks the corresponding references.
	 */
	public void trulyUnload() {
		UserObjectsCache.instance.unload(this);
		
		if (eProperties != null) {
			eProperties.setEContents(null);
			eProperties.setECrossReferences(null);
		}
		eSettings = null;
		eContainer = null;
		eAdapters = null;
	}
	
	private static Collection<EStructuralFeature> featuresWithIndexWarning = new HashSet<EStructuralFeature>();
	
	private void checkForReasonableNonIndexedValueSet(EStructuralFeature feature, EList<?> valueSet) {
		FragmentationType fragmentationType = EMFFragUtil.getFragmentationType(feature);
		if (fragmentationType == FragmentationType.None || fragmentationType == FragmentationType.FragmentsContainment) {
			if (valueSet.size() > 500) {
				if (!featuresWithIndexWarning.contains(feature)) {
					EmfFragActivator.instance.warning("A value set of feature " + feature.getName() + " of class "
							+ feature.getEContainingClass().getName()
							+ " has now more than 1000 values. Consider to make this feature indexed.");
					featuresWithIndexWarning.add(feature);
				}				
			}
		}
	}	

	@Override
	protected NotificationChain eDynamicInverseAdd(InternalEObject otherEnd, int featureID, NotificationChain msgs) {
	    EReference feature = (EReference)eClass().getEStructuralFeature(featureID);
	    EStructuralFeature opposite = feature.getEOpposite();
	    if (opposite != null && opposite.isMany()) {
	    	checkForReasonableNonIndexedValueSet(opposite, (EList<?>)otherEnd.eGet(opposite));
	    }
	    return super.eDynamicInverseAdd(otherEnd, featureID, msgs);
	}

	@Override
	protected EStructuralFeature.Internal.SettingDelegate eSettingDelegate(final EStructuralFeature eFeature) {
		FragmentationType type = EMFFragUtil.getFragmentationType(eFeature);
		if (type == FragmentationType.None || type == FragmentationType.FragmentsContainment) {
			return ((EStructuralFeature.Internal) eFeature).getSettingDelegate();
		} else {
			return new EStructuralFeatureImpl.InternalSettingDelegateMany(EStructuralFeatureImpl.InternalSettingDelegateMany.DATA_DYNAMIC, eFeature) {
				@Override
				protected Setting createDynamicSetting(InternalEObject owner) {
					int kind = EcoreEList.Generic.kind(eFeature);
					return new FValueSetList(kind, FInternalObjectImpl.class, FInternalObjectImpl.this, eFeature);
				}
			};
		}
	}

	void eBasicSetContainerForIndexClass(FInternalObjectImpl indexClass, URI uri) {	
		Resource eResource = eResource();
		
		if (!eIsProxy()) {
			// Do not update containment if the object is a proxy. In that case
			// the object is loaded and not modified by a user.
			indexClass.getFragmentation().createFragment(uri, this);
			updateAfterResourceChange(eResource);
		}
	}

	@Override
	protected void eBasicSetContainer(InternalEObject newContainer, int newContainerFeatureID) {
		Fragment oldFragment = getFragment();
		super.eBasicSetContainer(newContainer, newContainerFeatureID);
		
		if (!eIsProxy()) {
			// Do not update containment if the object is a proxy. In that case
			// the object is loaded and not modified by a user.
			updateAfterContainerChange(oldFragment);
		}
	}

	/**
	 * Called when a Java instance of this class is created.
	 */
	private void onLoad() {
		if (EmfFragActivator.instance.collectStatistics) {
			setLoaded(getLoaded() + 1);
		}
	}

	/**
	 * Called when the object is added to a fragmented model.
	 */
	private void onCreate() {
		if (EmfFragActivator.instance.collectStatistics) {
			long loaded = getLoaded();
			if (loaded > 0) {
				setLoaded(loaded - 1);
			}
		}
	}

	void onAccess() {
		if (EmfFragActivator.instance.collectStatistics) {
			setAccessed(getAccessed() + 1);
		}
		
		Fragment fragment = getFragment();
		if (fragment != null) {
			fragment.getFragmentedModel().touch(fragment);
		}
	}

	/**
	 * The default value of the '{@link #getId() <em>Id</em>}' attribute. <!--
	 * begin-user-doc --> <!-- end-user-doc -->
	 * 
	 * @see #getId()
	 * @generated
	 * @ordered
	 */
	protected static final String ID_EDEFAULT = null;

	/**
	 * The default value of the '{@link #getAccessed() <em>Accessed</em>}' attribute.
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @see #getAccessed()
	 * @generated
	 * @ordered
	 */
	protected static final long ACCESSED_EDEFAULT = 0L;
	/**
	 * The default value of the '{@link #getLoaded() <em>Loaded</em>}' attribute.
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @see #getLoaded()
	 * @generated
	 * @ordered
	 */
	protected static final long LOADED_EDEFAULT = 0L;

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	protected FInternalObjectImpl() {
		super();
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	@Override
	protected EClass eStaticClass() {
		return InternalPackage.Literals.FINTERNAL_OBJECT;
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * 
	 * @generated
	 */
	// @Override
	// protected EClass eStaticClass() {
	// return InternalPackage.Literals.FINTERNAL_OBJECT;
	// }

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	@Override
	protected int eStaticFeatureCount() {
		return 0;
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	public String getId() {
		return (String)eDynamicGet(InternalPackage.FINTERNAL_OBJECT__ID, InternalPackage.Literals.FINTERNAL_OBJECT__ID, true, true);
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	public void setId(String newId) {
		eDynamicSet(InternalPackage.FINTERNAL_OBJECT__ID, InternalPackage.Literals.FINTERNAL_OBJECT__ID, newId);
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	@SuppressWarnings("unchecked")
	public EList<EObject> getExtensions() {
		return (EList<EObject>)eDynamicGet(InternalPackage.FINTERNAL_OBJECT__EXTENSIONS, InternalPackage.Literals.FINTERNAL_OBJECT__EXTENSIONS, true, true);
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	public long getAccessed() {
		return (Long)eDynamicGet(InternalPackage.FINTERNAL_OBJECT__ACCESSED, InternalPackage.Literals.FINTERNAL_OBJECT__ACCESSED, true, true);
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	public void setAccessed(long newAccessed) {
		eDynamicSet(InternalPackage.FINTERNAL_OBJECT__ACCESSED, InternalPackage.Literals.FINTERNAL_OBJECT__ACCESSED, newAccessed);
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	public long getLoaded() {
		return (Long)eDynamicGet(InternalPackage.FINTERNAL_OBJECT__LOADED, InternalPackage.Literals.FINTERNAL_OBJECT__LOADED, true, true);
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	public void setLoaded(long newLoaded) {
		eDynamicSet(InternalPackage.FINTERNAL_OBJECT__LOADED, InternalPackage.Literals.FINTERNAL_OBJECT__LOADED, newLoaded);
	}

	/**
	 * <!-- begin-user-doc -->
	 * <!-- end-user-doc -->
	 * @generated
	 */
	@SuppressWarnings("unchecked")
	public EList<Long> getIndexes() {
		return (EList<Long>)eDynamicGet(InternalPackage.FINTERNAL_OBJECT__INDEXES, InternalPackage.Literals.FINTERNAL_OBJECT__INDEXES, true, true);
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	@Override
	public NotificationChain eInverseRemove(InternalEObject otherEnd, int featureID, NotificationChain msgs) {
		switch (featureID) {
			case InternalPackage.FINTERNAL_OBJECT__EXTENSIONS:
				return ((InternalEList<?>)getExtensions()).basicRemove(otherEnd, msgs);
		}
		return super.eInverseRemove(otherEnd, featureID, msgs);
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	@Override
	public Object eGet(int featureID, boolean resolve, boolean coreType) {
		switch (featureID) {
			case InternalPackage.FINTERNAL_OBJECT__ID:
				return getId();
			case InternalPackage.FINTERNAL_OBJECT__EXTENSIONS:
				return getExtensions();
			case InternalPackage.FINTERNAL_OBJECT__ACCESSED:
				return getAccessed();
			case InternalPackage.FINTERNAL_OBJECT__LOADED:
				return getLoaded();
			case InternalPackage.FINTERNAL_OBJECT__INDEXES:
				return getIndexes();
		}
		return super.eGet(featureID, resolve, coreType);
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	@SuppressWarnings("unchecked")
	@Override
	public void eSet(int featureID, Object newValue) {
		switch (featureID) {
			case InternalPackage.FINTERNAL_OBJECT__ID:
				setId((String)newValue);
				return;
			case InternalPackage.FINTERNAL_OBJECT__EXTENSIONS:
				getExtensions().clear();
				getExtensions().addAll((Collection<? extends EObject>)newValue);
				return;
			case InternalPackage.FINTERNAL_OBJECT__ACCESSED:
				setAccessed((Long)newValue);
				return;
			case InternalPackage.FINTERNAL_OBJECT__LOADED:
				setLoaded((Long)newValue);
				return;
			case InternalPackage.FINTERNAL_OBJECT__INDEXES:
				getIndexes().clear();
				getIndexes().addAll((Collection<? extends Long>)newValue);
				return;
		}
		super.eSet(featureID, newValue);
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	@Override
	public void eUnset(int featureID) {
		switch (featureID) {
			case InternalPackage.FINTERNAL_OBJECT__ID:
				setId(ID_EDEFAULT);
				return;
			case InternalPackage.FINTERNAL_OBJECT__EXTENSIONS:
				getExtensions().clear();
				return;
			case InternalPackage.FINTERNAL_OBJECT__ACCESSED:
				setAccessed(ACCESSED_EDEFAULT);
				return;
			case InternalPackage.FINTERNAL_OBJECT__LOADED:
				setLoaded(LOADED_EDEFAULT);
				return;
			case InternalPackage.FINTERNAL_OBJECT__INDEXES:
				getIndexes().clear();
				return;
		}
		super.eUnset(featureID);
	}

	/**
	 * <!-- begin-user-doc --> <!-- end-user-doc -->
	 * @generated
	 */
	@Override
	public boolean eIsSet(int featureID) {
		switch (featureID) {
			case InternalPackage.FINTERNAL_OBJECT__ID:
				return ID_EDEFAULT == null ? getId() != null : !ID_EDEFAULT.equals(getId());
			case InternalPackage.FINTERNAL_OBJECT__EXTENSIONS:
				return !getExtensions().isEmpty();
			case InternalPackage.FINTERNAL_OBJECT__ACCESSED:
				return getAccessed() != ACCESSED_EDEFAULT;
			case InternalPackage.FINTERNAL_OBJECT__LOADED:
				return getLoaded() != LOADED_EDEFAULT;
			case InternalPackage.FINTERNAL_OBJECT__INDEXES:
				return !getIndexes().isEmpty();
		}
		return super.eIsSet(featureID);
	}

	private URI fragmentURIForContainerChange = null;

	public void fragmentURIForContainerChange(URI uri) {
		assert (fragmentURIForContainerChange == null);		
		fragmentURIForContainerChange = uri;
	}
}