package ch.uzh.ifi.rerg.cs.dynanalysis;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

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.EDataType;
import org.eclipse.emf.ecore.EEnum;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EOperation;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.ETypedElement;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.EcoreUtil;

import ch.uzh.ifi.rerg.cs.metrics.MetamodelSize;
import ch.uzh.ifi.rerg.cs.metrics.ModelSize;
import ch.uzh.ifi.rerg.cs.metrics.Size;
import ch.uzh.ifi.rerg.kextra.CallEvent;
import ch.uzh.ifi.rerg.kextra.Event;
import ch.uzh.ifi.rerg.kextra.ExecutionTrace;
import ch.uzh.ifi.rerg.kextra.KExtraPackage;
import ch.uzh.ifi.rerg.kextra.OperationCallEvent;
import ch.uzh.ifi.rerg.kextra.PropertyCallEvent;

public class DynAnalysis {

	private ResourceSet resourceSet;
	private Resource traceResource;
	private RecordTable recordTable;
	private ExecutionTrace trace;
	private Set<EClass> classes;
	private Set<EReference> references;
	private Set<EAttribute> attributes;
	private Set<EOperation> operations;
	private Set<EDataType> datatypes;
	private Set<EEnum> enums;
	
	private DynAnalysis()
	{
		resourceSet = new ResourceSetImpl();
		operations = new HashSet<EOperation>();
		attributes = new HashSet<EAttribute>();
		references = new HashSet<EReference>();
		enums = new HashSet<EEnum>();
		classes = new HashSet<EClass>();
		datatypes = new HashSet<EDataType>();
		recordTable = new RecordTable();
	}

	public DynAnalysis(URI traceURI)
	{
		this();
		trace = loadTrace(traceURI);
		proceedTrace();
	}

	private ExecutionTrace loadTrace(URI uri)
	{
		traceResource = resourceSet.createResource(uri);
		
		try {
			traceResource.load(null);
			return (ExecutionTrace) EcoreUtil.getObjectByType(traceResource.getContents(), KExtraPackage.eINSTANCE.getExecutionTrace());
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

	private void proceedTrace() {
		for(Event evt: trace.getEvents())
		{
			proceedEvent(evt);
		}
	}

	@SuppressWarnings("unchecked")
	private void proceedEvent(Event evt) {
		if (evt instanceof CallEvent)
		{
			CallEvent ce = (CallEvent) evt;
			
			EObject target = ce.getTarget();
			if(target.eIsProxy())
			{
				//System.err.println("Warning: Unresolved Proxy: " + target);
				return;
			}
			if(!isObjectInModel(target))
			{
				//System.err.println("EObject not in model: " + target);
				return;
			}
			EClass targetClass = target.eClass();

			if (evt instanceof OperationCallEvent)
			{
				OperationCallEvent oce = (OperationCallEvent) evt;
				EOperation op = findOperationInEcore(oce.getFeature(), targetClass);
				
				if(op != null)
				{
					
					classes.add(targetClass);
					classes.add(op.getEContainingClass());		
					recordTable.record(target, op);
					operations.add(op);
				}	
			}
			else if (evt instanceof PropertyCallEvent)
			{
				PropertyCallEvent pce = (PropertyCallEvent) evt;
				EStructuralFeature feat = findPropertyInEcore(pce.getFeature(), targetClass);
				
				if(feat != null)
				{
					classes.add(targetClass);
					classes.add(feat.getEContainingClass());
					
					recordTable.record(target, feat);
					
					if (feat instanceof EAttribute) {
						EAttribute att = (EAttribute) feat;
						attributes.add(att);
						
						EDataType attType = att.getEAttributeType();
						
						if(attType instanceof EEnum)
						{
							enums.add((EEnum) attType);
						}
						else
						{
							datatypes.add(attType);
						}
						
					}
					else if (feat instanceof EReference)
					{
						EReference ref = (EReference) feat;
						references.add(ref);
						
						if(feat.isMany())
						{
							EList<EObject> linkedElements = (EList<EObject>) target.eGet(feat);
							for(EObject refObj: linkedElements)
							{
								if(isObjectInModel(refObj))
								{
									recordTable.record(refObj);
									classes.add(refObj.eClass());
								}
							}
						}
						else
						{
							EObject refObj = (EObject) target.eGet(feat);
							if(refObj != null && isObjectInModel(refObj))
							{
								recordTable.record(refObj);
								classes.add(refObj.eClass());
							}
						}
					}
				}
			}
		}
	}

	private boolean isObjectInModel(EObject target) {
		return target.eResource() != null && target.eResource().getURI().isPlatform();
	}
	
	private EStructuralFeature findPropertyInEcore(String name, EClass cl)
	{
		for(EStructuralFeature candidate : cl.getEAllStructuralFeatures())
		{
			if(name.equals(candidate.getName()))
				return candidate;
		}
		//System.err.println("Feature " + name + " not found.");
		return null;
	}

	private EOperation findOperationInEcore(String name, EClass cl)
	{
		for(EOperation candidate : cl.getEAllOperations())
		{
			if(name.equals(candidate.getName()))
				return candidate;
		}
		//System.err.println("Operation " + name + " not found.");
		return null;
	}

	public Size getDynamicModelFootprintSize()
	{
		int nObjects = recordTable.getTargets().size();
		int nPointers = 0;
		int nValues = 0;
		int nOperations = 0;
		
		for(ETypedElement feature: recordTable.getFeatures())
		{
			int n = recordTable.getTargets(feature).size();
			if(feature instanceof EOperation)
			{
				nOperations += n;
			}
			else if(feature instanceof EAttribute)
			{
				nValues += n;
			}
			else if(feature instanceof EReference)
			{
				nPointers += n;
			}
		}
		
		return new ModelSize(nObjects,nPointers,nValues,nOperations);
	}
	
	public Size getDynamicMetaModelFootprintSize()
	{
		int nClasses = classes.size();
		int nAttributes = attributes.size();
		int nReferences = references.size();
		int nEnums = enums.size();
		int nPrimitiveTypes = datatypes.size();
		int nOperations = operations.size();
		int nEnumLiterals = 0;
		for(EEnum en: enums)
		{
			nEnumLiterals += en.getELiterals().size();
		}
		return new MetamodelSize(nClasses,nAttributes,nReferences,nEnums,nEnumLiterals,nPrimitiveTypes,nOperations);
	}
	
	public boolean isFeatureUsed(ETypedElement el)
	{
		return recordTable.getFeatures().contains(el);
	}
	
	public boolean isObjectUsed(EObject el)
	{
		return recordTable.getTargets().contains(el);
	}
	
	public boolean isFeatureUsedOnTarget(ETypedElement feat, EObject obj)
	{
		return recordTable.exists(obj, feat);
	}

	public int getTraceLength() {
		return trace.getEvents().size();
	}
}
