/*******************************************************************************
 * Copyright (c) 2007, 2013 Wind River Systems, Inc. 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:
 *     Markus Schorn - initial API and implementation
 *******************************************************************************/ 
package org.eclipse.cdt.internal.ui.typehierarchy;

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

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;

import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.ICompositeType;
import org.eclipse.cdt.core.dom.ast.IEnumeration;
import org.eclipse.cdt.core.dom.ast.IType;
import org.eclipse.cdt.core.dom.ast.ITypedef;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPBase;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassType;
import org.eclipse.cdt.core.index.IIndex;
import org.eclipse.cdt.core.index.IIndexBinding;
import org.eclipse.cdt.core.index.IIndexName;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.ui.CUIPlugin;

import org.eclipse.cdt.internal.core.dom.parser.cpp.ClassTypeHelper;
import org.eclipse.cdt.internal.core.model.ext.ICElementHandle;

import org.eclipse.cdt.internal.ui.viewsupport.IndexUI;

class THGraph {
	private static final ICElement[] NO_MEMBERS = {};
	private THGraphNode fInputNode;
	private HashSet<THGraphNode> fRootNodes= new HashSet<>();
	private HashSet<THGraphNode> fLeafNodes= new HashSet<>();
	private HashMap<ICElement, THGraphNode> fNodes= new HashMap<>();
	private boolean fFileIsIndexed;
	
	public THGraph() {
	}

	public THGraphNode getInputNode() {
		return fInputNode;
	}
	
	public THGraphNode getNode(ICElement elem) {
		return fNodes.get(elem);
	}

	private THGraphNode addNode(ICElement input) {
		THGraphNode node= fNodes.get(input); 

		if (node == null) {
			node= new THGraphNode(input);
			fNodes.put(input, node);
			fRootNodes.add(node);
			fLeafNodes.add(node);
		}
		return node;
	}
	
	private THGraphEdge addEdge(THGraphNode from, THGraphNode to) {
		if (createsLoopOrIsDuplicate(from, to)) {
			return null;
		}
		THGraphEdge edge= new THGraphEdge(from, to);
		from.startEdge(edge);
		to.endEdge(edge);
		fRootNodes.remove(to);
		fLeafNodes.remove(from);
		return edge;
	}

	private boolean createsLoopOrIsDuplicate(THGraphNode from, THGraphNode to) {
		if (from == to) {
			return true;
		}
		if (to.getOutgoing().isEmpty() || from.getIncoming().isEmpty()) {
			return false;
		}
		
		HashSet<THGraphNode> checked= new HashSet<>();
		ArrayList<THGraphNode> stack= new ArrayList<>();
		stack.add(to);
		
		while (!stack.isEmpty()) {
			THGraphNode node= stack.remove(stack.size() - 1);
			List<THGraphEdge> out= node.getOutgoing();
			for (THGraphEdge edge : out) {
				node= edge.getEndNode();
				if (node == from) {
					return true;
				}
				if (checked.add(node)) {
					stack.add(node);
				}
			}
		}
		// Check if edge is already there.
		List<THGraphEdge> out= from.getOutgoing();
		for (THGraphEdge edge : out) {
			if (edge.getEndNode() == to) {
				return true;
			}
		}
		return false;
	}

	public Collection<THGraphNode> getRootNodes() {
		return fRootNodes;
	}

	public Collection<THGraphNode> getLeafNodes() {
		return fLeafNodes;
	}

	public void defineInputNode(IIndex index, ICElement input) {
		if (input != null) {
			try {
				if (IndexUI.isIndexed(index, input)) {
					fFileIsIndexed= true;
					input= IndexUI.attemptConvertionToHandle(index, input);
					fInputNode= addNode(input);
				}
			} catch (CoreException e) {
				CUIPlugin.log(e);
			}
		}
	}

	public void addSuperClasses(IIndex index, IProgressMonitor monitor) {
		if (fInputNode == null) {
			return;
		}
		HashSet<ICElement> handled= new HashSet<>();
		ArrayList<ICElement> stack= new ArrayList<>();
		stack.add(fInputNode.getElement());
		handled.add(fInputNode.getElement());
		while (!stack.isEmpty()) {
			if (monitor.isCanceled()) {
				return;
			}
			ICElement elem= stack.remove(stack.size() - 1);
			THGraphNode graphNode= addNode(elem);
			try {
				IIndexBinding binding = IndexUI.elementToBinding(index, elem);
				if (binding != null) {
					addMembers(index, graphNode, binding);
				}
				if (binding instanceof ICPPClassType) {
					ICPPClassType ct= (ICPPClassType) binding;
					ICPPBase[] bases= ct.getBases();
					for (ICPPBase base : bases) {
						if (monitor.isCanceled()) {
							return;
						}
						IType baseType= base.getBaseClassType();
						if (baseType instanceof IBinding) {
							final IBinding baseBinding = (IBinding) baseType;
							ICElementHandle[] baseElems= IndexUI.findRepresentative(index, baseBinding);
							for (ICElementHandle baseElem : baseElems) {
								THGraphNode baseGraphNode= addNode(baseElem);
								addMembers(index, baseGraphNode, baseBinding);							
								addEdge(graphNode, baseGraphNode);
								if (handled.add(baseElem)) {
									stack.add(baseElem);
								}
							}
						}
					}
				} else if (binding instanceof ITypedef) {
					ITypedef ct= (ITypedef) binding;
					IType type= ct.getType();
					if (type instanceof IBinding) {
						IBinding basecl= (IBinding) type;
						ICElementHandle[] baseElems= IndexUI.findRepresentative(index, basecl);
						if (baseElems.length > 0) {
							ICElementHandle baseElem= baseElems[0];
							THGraphNode baseGraphNode= addNode(baseElem);
							addMembers(index, baseGraphNode, basecl);							
							addEdge(graphNode, baseGraphNode);
							if (handled.add(baseElem)) {
								stack.add(baseElem);
							}
						}
					}
				}
			} catch (CoreException e) {
				CUIPlugin.log(e);
			}
		}
	}

	public void addSubClasses(IIndex index, IProgressMonitor monitor) {
		if (fInputNode == null) {
			return;
		}
		HashSet<ICElement> handled= new HashSet<>();
		ArrayList<ICElement> stack= new ArrayList<>();
		ICElement element = fInputNode.getElement();
		stack.add(element);
		handled.add(element);
		while (!stack.isEmpty()) {
			if (monitor.isCanceled()) {
				return;
			}
			ICElement elem= stack.remove(stack.size() - 1);
			THGraphNode graphNode= addNode(elem);
			try {
				IBinding binding = IndexUI.elementToBinding(index, elem);
				if (binding != null) {
					IIndexName[] names= index.findNames(binding, IIndex.FIND_REFERENCES | IIndex.FIND_DEFINITIONS);
					for (IIndexName indexName : names) {
						if (monitor.isCanceled()) {
							return;
						}
						if (indexName.isBaseSpecifier()) {
							IIndexName subClassDef= indexName.getEnclosingDefinition();
							if (subClassDef != null) {
								IBinding subClass= index.findBinding(subClassDef);
								ICElementHandle[] subClassElems= IndexUI.findRepresentative(index, subClass);
								if (subClassElems.length > 0) {
									ICElementHandle subClassElem= subClassElems[0];
									THGraphNode subGraphNode= addNode(subClassElem);
									addMembers(index, subGraphNode, subClass);							
									addEdge(subGraphNode, graphNode);
									if (handled.add(subClassElem)) {
										stack.add(subClassElem);
									}
								}
							}
						}
					}
				}
			} catch (CoreException e) {
				CUIPlugin.log(e);
			}
		}
	}
	
	private void addMembers(IIndex index, THGraphNode graphNode, IBinding binding) throws CoreException {
		if (graphNode.getMembers(false) == null) {
			ArrayList<ICElement> memberList= new ArrayList<>();
			if (binding instanceof ICPPClassType) {
				ICPPClassType ct= (ICPPClassType) binding;
				IBinding[] members= ClassTypeHelper.getDeclaredFields(ct, null);
				addMemberElements(index, members, memberList);
				members= ClassTypeHelper.getDeclaredMethods(ct, null);
				addMemberElements(index, members, memberList);
			} else if (binding instanceof ICompositeType) {
				ICompositeType ct= (ICompositeType) binding;
				IBinding[] members= ct.getFields();
				addMemberElements(index, members, memberList);
			} else if (binding instanceof IEnumeration) {
				IEnumeration ct= (IEnumeration) binding;
				IBinding[] members= ct.getEnumerators();
				addMemberElements(index, members, memberList);
			}

			if (memberList.isEmpty()) {
				graphNode.setMembers(NO_MEMBERS);
			} else {
				graphNode.setMembers(memberList.toArray(new ICElement[memberList.size()]));
			}
		}
	}
	
	private void addMemberElements(IIndex index, IBinding[] members, List<ICElement> memberList) 
			throws CoreException {
		for (IBinding binding : members) {
			ICElement[] elems= IndexUI.findRepresentative(index, binding);
			if (elems.length > 0) {
				memberList.add(elems[0]);
			}
		}
	}

	public boolean isTrivial() {
		return fNodes.size() <= 1;
	}

	public boolean isFileIndexed() {
		return fFileIsIndexed;
	}
}
