/*******************************************************************************
 * Copyright (c) 2012, 2014 UT-Battelle, LLC.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Initial API and implementation and/or initial documentation - Jay Jay Billings,
 *   Jordan H. Deyton, Dasha Gorin, Alexander J. McCaskey, Taylor Patterson,
 *   Claire Saunders, Matthew Wang, Anna Wojtowicz
 *******************************************************************************/
package org.eclipse.ice.datastructures.form.emf;

import java.util.ArrayList;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.ice.datastructures.form.DataComponent;
import org.eclipse.ice.datastructures.form.TreeComposite;

/**
 * 
 * The EMFTreeComposite is a subclass of TreeComposite that represents a tree node in
 * an Eclipse Modeling Framework Ecore model tree. To do that, it takes as input
 * the EClass metadata representing the corresponding model tree node, and from
 * that creates an actual EObject instance. From this data, the EMFTreeComposite
 * can construct an active data node (DataComponent) that contains EMFEntrys
 * corresponding to the EAttributes within the EClass metadata's
 * EStructuralFeatures. This data node can then be shown to the users in the
 * same way a regular TreeComposite does.
 * 
 * Additionally, since a TreeComposite can have child exemplars, so too can the
 * underlying Ecore model tree. To keep the model in sync, EMFTreeComposite
 * keeps track of the list of EClass EReferences that allow it to correctly add
 * and remove Ecore model nodes whenever a new child is added to this
 * TreeComposite.
 * 
 * @author Alex McCaskey
 * 
 */
@XmlRootElement(name = "EMFTreeComposite")
@XmlAccessorType(XmlAccessType.FIELD)
public class EMFTreeComposite extends TreeComposite {

	/**
	 * Reference to the Ecore model object that is the actual tree node instance
	 * and has sub-children. This reference is used and manipulated to keep the
	 * TreeComposite data in sync with the Ecore model tree.
	 */
	@XmlTransient
	private EObject ecoreNode;

	/**
	 * Reference to the EClass metadata corresponding to the ecoreNode EObject
	 * instance. This object gives information about the EStructuralFeatures
	 * (EAttributes and EReferences) of the ecoreNode EObject.
	 */
	@XmlTransient
	private EClass ecoreNodeMetaData;

	/**
	 * The nullary-constructor
	 */
	public EMFTreeComposite() {
		super();
		setName("Null Constructed EMFTreeComposite");
		setDescription(toString());
		return;
	}

	/**
	 * The constructor, takes a Ecore model tree EClass metadata object.
	 * 
	 * @param eClass
	 */
	public EMFTreeComposite(EClass eClass) {
		super();
		// Set the data
		if (eClass != null) {
			ecoreNodeMetaData = eClass;
			ecoreNode = EcoreUtil.create(ecoreNodeMetaData);
			setName(ecoreNodeMetaData.getName());
			setDescription(toString());

			// Create the active DataComponent
			createActiveDataNode();
		}

		return;
	}

	/**
	 * The constructor, takes a Ecore model tree EObject node instance
	 * 
	 * @param treeNode
	 */
	public EMFTreeComposite(EObject treeNode) {
		super();
		// Set the data
		ecoreNode = treeNode;
		ecoreNodeMetaData = ecoreNode.eClass();
		setName(ecoreNodeMetaData.getName());
		setDescription(toString());

		// Create the active DataComponent
		createActiveDataNode();

		return;
	}

	/**
	 * This method reads through this ECore node's EAttributes and creates an
	 * active DataComponent data node from them.
	 */
	private void createActiveDataNode() {
		// Local Declarations
		int id = 0;
		DataComponent data = new DataComponent();

		// Loop over all the EStructuralFeatures and pick out
		// the EAttributes to create Entries.
		for (EAttribute a : ecoreNodeMetaData.getEAllAttributes()) {
			if (!"mixed".equals(a.getName())) {
				// Create the EMFEntry to add to the DataComponent
				EMFEntry entry = new EMFEntry(a, ecoreNode);
				entry.setId(id++);

				// Add the Entry to the DataComponent and
				// register this EMFTreeComposite as a listener
				data.addEntry(entry);
			}
		}

		// Set the name and description for the DataComponent
		data.setName(ecoreNodeMetaData.getName() + " Data");
		data.setDescription("Please set all required data pertinent to "
				+ ecoreNodeMetaData.getName() + ".");

		// Set the DataComponent as the active data node.
		allowActiveDataNodes(true);
		addComponent(data);
		setActiveDataNode(data);

	}

	/**
	 * This method overrides TreeComposite.getChildExemplars to dynamically
	 * generate a list of exemplar children based on the EClass metadata's list
	 * of EReferences.
	 * 
	 * @return
	 */
	@Override
	public ArrayList<TreeComposite> getChildExemplars() {
		ArrayList<TreeComposite> exemplars = new ArrayList<TreeComposite>();
		boolean childExists = false;

		// Here are the rules here:
		// (1) Upper bound could be 1, in which case we can only
		// have 1 of that particular node in the tree, so we search here
		// to make sure it hasn't already been added
		// (2) The bound could be -1, in which case we can have as many
		// of
		// these nodes as the user wants, so no need to search here and
		// just let clients add a new child
		// (3) The bound could be -2, which is weird, but it means
		// unspecified,
		// so just treat it like (1).

		for (EReference exemplar : ecoreNodeMetaData.getEAllReferences()) { // BUG, need getE(ALL)References...

			// We don't want to use any of these types. 
			if ("EStringToStringMapEntry".equals(exemplar.getEReferenceType()
					.getName())) {
				continue;
			}

			// Get the upper bound
			int upperBound = exemplar.getUpperBound();

			// If -1, it is unbounded.
			if (upperBound == -1) {
				exemplars
						.add(new EMFTreeComposite(exemplar.getEReferenceType()));
				continue;
			} else if (upperBound <= 1) {

				// Loop through the children, if the current possible exemplar
				// has the
				// same name as one of this tree's children, then do not add it
				// to the exemplar list.
				for (TreeComposite currentChild : children) {
					if (currentChild.getName().equals(
							exemplar.getEReferenceType().getName())) {
						childExists = true;
						break;
					}
				}

				// If we didn't find an already existing child, then add it
				if (!childExists) {
					exemplars.add(new EMFTreeComposite(exemplar
							.getEReferenceType()));
				}

			}

		}
		return exemplars;
	}

	/**
	 * Return the EObject tree node corresponding to this EMFTreeComposite.
	 * 
	 * @return the EObject representing the root node of this tree
	 */
	public EObject getEcoreNode() {
		return ecoreNode;
	}

	/**
	 * Return the EClass metadata object corresponding to this EMFTreeComposite.
	 * 
	 * @return
	 */
	public EClass getEcoreMetaData() {
		return ecoreNodeMetaData;
	}

	/**
	 * This method overrides TreeComposite.setNextChild to allow more than one
	 * of the same child TreeComposite to be added to this EMFTreeComposite and
	 * append a Ecore child Exemplar EObject to the containingNode EObject.
	 * 
	 * @param cNode
	 */
	@Override
	public void setNextChild(TreeComposite cNode) {

		// Local Declarations
		EMFTreeComposite castedTree;
		EReference exemplar;
		int currentSize = children.size();

		// Set the next child. This will use our custom checkExemplars method
		super.setNextChild(cNode);

		// Check if we successfully added a child
		if (currentSize == children.size()) {
			logger.info("Could not add " + cNode.getName()
					+ " as child of " + getName());
			return;
		}

		castedTree = (EMFTreeComposite) cNode;

		// EObject newObj = EcoreUtil.create(castedTree.ecoreNodeMetaData);

		// We need to figure out which child exemplar this child node
		// corresponds to.
		exemplar = getEReferenceExemplar(castedTree.getName());

		if (exemplar != null) {
			if (exemplar.getUpperBound() != -1) {
				ecoreNode.eSet(exemplar, castedTree.ecoreNode);
			} else {
				((EList<EObject>) ecoreNode.eGet(exemplar))
						.add(castedTree.ecoreNode);
			}
		}

		// We have to update the Containing EObject, and
		// corresponding EMFEntries that use it, on
		// the newly added child cNode.
		castedTree.updateContainingNode(castedTree.ecoreNode);
	}

	/**
	 * This method returns an EReference exemplar child of this EClass node c
	 * corresponding to the given String name.
	 * 
	 * @param treeName
	 * @return
	 */
	private EReference getEReferenceExemplar(String treeName) {
		for (EReference ref : this.ecoreNodeMetaData.getEReferences()) {
			if (treeName.equals(ref.getEReferenceType().getName())) {
				return ref;
			}
		}

		return null;
	}

	/**
	 * This method overrides TreeComposite.checkExemplars to use the overridden
	 * getChildExemplars method to see if a given TreeComposite can be added as
	 * a new child of this tree.
	 * 
	 * @param cNode
	 * @return
	 */
	@Override
	protected boolean checkExemplars(TreeComposite cNode) {

		// Loop over all exemplars and see if this cNode is compatible
		for (TreeComposite exemplar : getChildExemplars()) {
			if (cNode.getName().equals("DefinitionType")) {
				logger.info("Exemplar: " + exemplar.getName());
			}
			if (cNode.getName().equals(exemplar.getName())) {
				return true;
			}
		}

		return false;
	}

	/**
	 * This method overrides TreeComposite.hasChildExemplars to use the
	 * overridden getChildExemplars method to return whether or not this tree
	 * has child exemplars.
	 * 
	 * @return
	 */
	@Override
	public boolean hasChildExemplars() {
		return !getChildExemplars().isEmpty();
	}

	/**
	 * Update the EObject node and the EMFEntries corresponding to that EObjects
	 * contained EAttributes.
	 * 
	 * @param newEcoreChild
	 */
	public void updateContainingNode(EObject newEcoreChild) {
		// Get the Active Data Node, there should just be one
		// Clear its Entries since we need to start anew
		// with EMFEntries that point to the correct containing Ecore node
		DataComponent dataComp = (DataComponent) getDataNodes().get(0);
		dataComp.clearEntries();

		ecoreNode = newEcoreChild;

		// Loop over the new Ecore Node's attributes and
		// create a EMFEntry for each
		for (EAttribute a : ecoreNode.eClass().getEAllAttributes()) {
			dataComp.addEntry(new EMFEntry(a, ecoreNode));
		}

		// Set the active data node data.
		allowActiveDataNodes(true);
		setActiveDataNode(dataComp);
	}

	/**
	 * This method overrides TreeComposite.removeChild to also remove the
	 * corresponding EObject child node.
	 * 
	 * @param cNode
	 */
	@Override
	public void removeChild(TreeComposite cNode) {
		// Local Declarations
		EStructuralFeature childStructuralFeature = null;
		int currentSize = children.size();

		// Remove the TreeComposite from this TreeComposite
		super.removeChild(cNode);

		// If it worked...
		if (currentSize != children.size()) {

			// Get the EStructuralFeature that we are removing
			for (EStructuralFeature feature : ecoreNodeMetaData
					.getEStructuralFeatures()) {
				if (feature.getEType().getName().equals(cNode.getName())) {
					childStructuralFeature = feature;
				}
			}

			// Remove it.
			((EList<EObject>) ecoreNode.eGet(childStructuralFeature))
					.remove(((EMFTreeComposite) cNode).getEcoreNode());
		}

		return;
	}

	/**
	 * This operation is used to check equality between the EMFTreeComposite and
	 * another EMFTreeComposite. It returns true if the EMFTreeComposites are
	 * equal and false if they are not.
	 */
	@Override
	public boolean equals(Object otherTreeComposite) {
		return super.equals(otherTreeComposite);
	}

	/**
	 * This operation returns the hashcode value of the EMFTreeComposite. It
	 * does not include the parent and sibling references when computing the
	 * hashcode.
	 * 
	 * @return
	 */
	@Override
	public int hashCode() {
		return super.hashCode();
	}

	/**
	 * Sets the ECore node meta data
	 * 
	 * @param metaData : The meta data to set
	 */
	public void setECoreNodeMetaData(EClass metaData) {
		ecoreNodeMetaData = metaData;
	}
	
	/**
	 * This operation performs a deep copy of the attributes of another
	 * EMFTreeComposite into the current EMFTreeComposite. It copies ALL of the
	 * children of the EMFTreeComposite, data and child nodes alike.
	 * 
	 * @param otherTreeComposite
	 */
	public void copy(EMFTreeComposite otherTreeComposite) {

		// If null, return
		if (otherTreeComposite == null) {
			return;
		}

		if (otherTreeComposite.ecoreNodeMetaData != null) {
			ecoreNodeMetaData = otherTreeComposite.ecoreNodeMetaData;
			ecoreNode = EcoreUtil.create(ecoreNodeMetaData);
		}
		super.copy(otherTreeComposite, true);

		return;
	}

	/**
	 * This operation provides a deep copy of the TreeComposite. It clones ALL
	 * of the nodes of a TreeComposite, data and child nodes alike.
	 */
	@Override
	public Object clone() {
		// Local Declarations
		EMFTreeComposite emfTreeComposite = new EMFTreeComposite();

		// Copy this EMFTreeComposite
		emfTreeComposite.copy(this);

		// Return the tree
		return emfTreeComposite;
	}

}