/*******************************************************************************
 * Copyright (c) 2004, 2009 Tasktop Technologies and others.
 * 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:
 *     Tasktop Technologies - initial API and implementation
 *******************************************************************************/
package org.eclipse.mylyn.internal.context.core;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.mylyn.context.core.IInteractionContext;
import org.eclipse.mylyn.context.core.IInteractionContextScaling;
import org.eclipse.mylyn.context.core.IInteractionElement;
import org.eclipse.mylyn.monitor.core.InteractionEvent;

/**
 * @author Mik Kersten
 * @author Shawn Minto
 * @author David Green bug 257977 isInteresting
 */
public class InteractionContext implements IInteractionContext {

	private String handleIdentifier;

	private final List<InteractionEvent> interactionHistory;

	private final Map<String, InteractionContextElement> elementMap;

	private final Map<String, IInteractionElement> landmarkMap;

	/**
	 * The last element that was added to this context.
	 */
	private InteractionContextElement activeNode;

	private InteractionEvent lastEdgeEvent;

	private InteractionContextElement lastEdgeNode;

	private String contentLimitedTo;

	private int numUserEvents;

	private final IInteractionContextScaling contextScaling;

	public InteractionContext(String id, IInteractionContextScaling scaling) {
		this.handleIdentifier = id;
		this.contextScaling = scaling;
		this.interactionHistory = new ArrayList<InteractionEvent>();
		this.elementMap = new HashMap<String, InteractionContextElement>();
		this.landmarkMap = new HashMap<String, IInteractionElement>();

		for (InteractionEvent event : interactionHistory) {
			parseInteractionEvent(event);
		}

		for (InteractionContextElement node : elementMap.values()) {
			if (node.getInterest().isLandmark()) {
				landmarkMap.put(node.getHandleIdentifier(), node);
			}
		}

		activeNode = lastEdgeNode;
	}

	public synchronized void addEvents(IInteractionContext otherContext) {
		for (InteractionEvent event : otherContext.getInteractionHistory()) {
			parseEvent(event);
		}
	}

	public synchronized IInteractionElement parseEvent(InteractionEvent event) {
		interactionHistory.add(event);
		return parseInteractionEvent(event);
	}

	/**
	 * Propagations and predictions are not added as edges
	 */
	private IInteractionElement parseInteractionEvent(InteractionEvent event) {
		if (event.getStructureHandle() == null || event.getKind() == null) {
			return null;
		}

		if (event.getKind().isUserEvent()) {
			numUserEvents++;
		}

		InteractionContextElement node = elementMap.get(event.getStructureHandle());
		if (node == null) {
			if (event instanceof AggregateInteractionEvent) {
				node = new InteractionContextElement(event.getStructureKind(), event.getStructureHandle(), this,
						((AggregateInteractionEvent) event).getEventCountOnCreation());
			} else {
				node = new InteractionContextElement(event.getStructureKind(), event.getStructureHandle(), this);
			}
			elementMap.put(event.getStructureHandle(), node);
		}

		if (event.getKind().isUserEvent() && event instanceof AggregateInteractionEvent) {
			// add the rest of the events that this event represented
			numUserEvents += ((AggregateInteractionEvent) event).getNumCollapsedEvents() - 1;
		}

		if (event.getNavigation() != null
				&& !event.getNavigation().equals("null") && lastEdgeEvent != null //$NON-NLS-1$
				&& lastEdgeNode != null && lastEdgeEvent.getStructureHandle() != null
				&& event.getKind() != InteractionEvent.Kind.PROPAGATION
				&& event.getKind() != InteractionEvent.Kind.PREDICTION) {
			IInteractionElement navigationSource = elementMap.get(lastEdgeEvent.getStructureHandle());
			if (navigationSource != null) {
				InteractionContextRelation edge = lastEdgeNode.getRelation(event.getStructureHandle());
				if (edge == null) {
					edge = new InteractionContextRelation(event.getStructureKind(), event.getNavigation(),
							lastEdgeNode, node, this);
					lastEdgeNode.addEdge(edge);
				}
				DegreeOfInterest doi = (DegreeOfInterest) edge.getInterest();
				doi.addEvent(event);
			}
		}
		DegreeOfInterest doi = (DegreeOfInterest) node.getInterest();

		doi.addEvent(event);
		if (doi.isLandmark()) {
			landmarkMap.put(node.getHandleIdentifier(), node);
		} else {
			landmarkMap.remove(node.getHandleIdentifier()); // TODO: redundant
		}
		if (event.getKind().isUserEvent()) {
			lastEdgeEvent = event;
			lastEdgeNode = node;
			activeNode = node;
		}
		return node;
	}

	public synchronized IInteractionElement get(String elementHandle) {
		if (elementHandle == null) {
			return null;
		} else {
			return elementMap.get(elementHandle);
		}
	}

	public synchronized boolean isInteresting(String elementHandle) {
		InteractionContextElement element = elementMap.get(elementHandle);
		if (element != null) {
			return element.getInterest().isInteresting();
		}
		return false;
	}

	public synchronized List<IInteractionElement> getInteresting() {
		List<IInteractionElement> elements = new ArrayList<IInteractionElement>();
		for (String key : elementMap.keySet()) {
			InteractionContextElement info = elementMap.get(key);
			if (info != null && info.getInterest().isInteresting()) {
				elements.add(info);
			}
		}
		return elements;
	}

	public synchronized List<IInteractionElement> getLandmarks() {
		return new ArrayList<IInteractionElement>(landmarkMap.values());
	}

	public synchronized void updateElementHandle(IInteractionElement element, String newHandle) {
		InteractionContextElement currElement = elementMap.remove(element.getHandleIdentifier());
		if (currElement != null) {
			currElement.setHandleIdentifier(newHandle);
			elementMap.put(newHandle, currElement);
		}
	}

	public synchronized IInteractionElement getActiveNode() {
		return activeNode;
	}

	public synchronized void delete(Collection<IInteractionElement> nodes) {
		// remove elements
		Set<String> handlesToRemove = new HashSet<String>();
		for (IInteractionElement node : nodes) {
			handlesToRemove.add(node.getHandleIdentifier());
			landmarkMap.remove(node.getHandleIdentifier());
			elementMap.remove(node.getHandleIdentifier());

			if (activeNode != null && node.getHandleIdentifier().equals(activeNode.getHandleIdentifier())) {
				activeNode = null;
			}
		}

		// remove events
		List<InteractionEvent> eventsToRemove = new ArrayList<InteractionEvent>();
		for (InteractionEvent event : interactionHistory) {
			if (handlesToRemove.contains(event.getStructureHandle())) {
				eventsToRemove.add(event);
			}
		}
		interactionHistory.removeAll(eventsToRemove);
	}

	public synchronized void delete(IInteractionElement node) {
		delete(Collections.singleton(node));
	}

	public synchronized List<IInteractionElement> getAllElements() {
		return new ArrayList<IInteractionElement>(elementMap.values());
	}

	public String getHandleIdentifier() {
		return handleIdentifier;
	}

	/**
	 * @since 2.1
	 */
	public void setHandleIdentifier(String handle) {
		this.handleIdentifier = handle;
	}

	@Override
	public String toString() {
		return handleIdentifier;
	}

	public synchronized void reset() {
		elementMap.clear();
		interactionHistory.clear();
		landmarkMap.clear();
		activeNode = null;
		numUserEvents = 0;
		lastEdgeEvent = null;
		lastEdgeNode = null;
	}

	public synchronized int getUserEventCount() {
		return numUserEvents;
	}

	public synchronized List<InteractionEvent> getInteractionHistory() {
		return new ArrayList<InteractionEvent>(interactionHistory);
	}

	public synchronized void collapse() {
		collapseHistory(interactionHistory);
	}

	private synchronized void collapseHistory(List<InteractionEvent> interactionHistoryToCollapseTo) {
		List<InteractionEvent> collapsedHistory = new ArrayList<InteractionEvent>();
		for (InteractionContextElement node : elementMap.values()) {
			if (!node.equals(activeNode)) {
				collapseNode(collapsedHistory, node);
			}
		}
		if (activeNode != null) {
			collapseNode(collapsedHistory, activeNode);
		}

		interactionHistoryToCollapseTo.clear();
		interactionHistoryToCollapseTo.addAll(collapsedHistory);
	}

	/**
	 * This method is only used for asynchronous saving of task contexts
	 * 
	 * @return a context with a copy of the collapsed interaction history. All other fields are not set.
	 */
	synchronized IInteractionContext createCollapsedWritableCopy() {
		InteractionContext copiedContext = new InteractionContext(handleIdentifier, contextScaling);
		copiedContext.contentLimitedTo = contentLimitedTo;

		collapseHistory(copiedContext.interactionHistory);

		// none of the following are used for writing contexts so we aren't going to try to copy them
		//copiedContext.numUserEvents = numUserEvents;
		// copiedContext.lastEdgeNode = lastEdgeNode;
		// copiedContext.lastEdgeEvent = lastEdgeEvent;
		// copiedContext.landmarkMap = landmarkMap;
		// copiedContext.elementMap = elementMap;
		// copiedContext.activeNode = activeNode;

		return copiedContext;
	}

	private void collapseNode(List<InteractionEvent> collapsedHistory, InteractionContextElement node) {
		collapsedHistory.addAll(((DegreeOfInterest) node.getInterest()).getCollapsedEvents());
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		InteractionContext other = (InteractionContext) obj;
		if (contentLimitedTo == null) {
			if (other.contentLimitedTo != null) {
				return false;
			}
		} else if (!contentLimitedTo.equals(other.contentLimitedTo)) {
			return false;
		}
		if (handleIdentifier == null) {
			if (other.handleIdentifier != null) {
				return false;
			}
		} else if (!handleIdentifier.equals(other.handleIdentifier)) {
			return false;
		}
		return true;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((contentLimitedTo == null) ? 0 : contentLimitedTo.hashCode());
		result = prime * result + ((handleIdentifier == null) ? 0 : handleIdentifier.hashCode());
		return result;
	}

	public IInteractionContextScaling getScaling() {
		return contextScaling;
	}

	public String getContentLimitedTo() {
		return contentLimitedTo;
	}

	public void setContentLimitedTo(String contentLimitedTo) {
		this.contentLimitedTo = contentLimitedTo;
	}
}
