package org.objectstyle.wolips.core.resources.types;

import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaElementDelta;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.compiler.util.SuffixConstants;
import org.eclipse.jdt.internal.core.JarPackageFragmentRoot;
import org.eclipse.jdt.internal.core.JavaElement;
import org.eclipse.jdt.internal.core.JavaModel;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.core.JavaProject;
import org.eclipse.jdt.internal.core.hierarchy.TypeHierarchy;
import org.eclipse.jdt.internal.core.search.AbstractSearchScope;

/**
 * Scope limited to the subtype and supertype hierarchy of a given type.
 */
public class WOHierarchyScope extends AbstractSearchScope implements SuffixConstants {
	private IJavaProject javaProject;
	
	public IType focusType;
	private String focusPath;

	private ITypeHierarchy hierarchy;
	private IType[] types;
	private Set resourcePaths;
	private IPath[] enclosingProjectsAndJars;

	protected IResource[] elements;
	protected int elementCount;

	public boolean needsRefresh;

	/* (non-Javadoc)
	 * Adds the given resource to this search scope.
	 */
	public void add(IResource element) {
		if (this.elementCount == this.elements.length) {
			System.arraycopy(
				this.elements,
				0,
				this.elements = new IResource[this.elementCount * 2],
				0,
				this.elementCount);
		}
		this.elements[this.elementCount++] = element;
	}

	/* (non-Javadoc)
	 * Creates a new hiearchy scope for the given type.
	 */
	public WOHierarchyScope(IType type, IJavaProject javaProject) throws JavaModelException {
		this.focusType = type;
		this.javaProject = javaProject;

		this.enclosingProjectsAndJars = computeProjectsAndJars(type);

		// resource path
		IPackageFragmentRoot root = (IPackageFragmentRoot)type.getPackageFragment().getParent();
		if (root.isArchive()) {
			IPath jarPath = root.getPath();
			Object target = JavaModel.getTarget(jarPath, true);
			String zipFileName;
			if (target instanceof IFile) {
				// internal jar
				zipFileName = jarPath.toString();
			} else if (target instanceof File) {
				// external jar
				zipFileName = ((File)target).getPath();
			} else {
				return; // unknown target
			}
			this.focusPath =
				zipFileName
					+ JAR_FILE_ENTRY_SEPARATOR
					+ type.getFullyQualifiedName().replace('.', '/')
					+ SUFFIX_STRING_class;
		} else {
			this.focusPath = type.getPath().toString();
		}

		this.needsRefresh = true;

		//disabled for now as this could be expensive
		//JavaModelManager.getJavaModelManager().rememberScope(this);
	}
	private void buildResourceVector() {
		HashMap resources = new HashMap();
		HashMap paths = new HashMap();
		this.types = this.hierarchy.getAllTypes();
		for (int i = 0; i < this.types.length; i++) {
			IType type = this.types[i];
			IResource resource = ((JavaElement) type).resource();
			if (resource != null && resources.get(resource) == null) {
				resources.put(resource, resource);
				add(resource);
			}
			IPackageFragmentRoot root =
				(IPackageFragmentRoot) type.getPackageFragment().getParent();
			if (root instanceof JarPackageFragmentRoot) {
				// type in a jar
				JarPackageFragmentRoot jar = (JarPackageFragmentRoot) root;
				IPath jarPath = jar.getPath();
				Object target = JavaModel.getTarget(jarPath, true);
				String zipFileName;
				if (target instanceof IFile) {
					// internal jar
					zipFileName = jarPath.toString();
				} else if (target instanceof File) {
					// external jar
					zipFileName = ((File)target).getPath();
				} else {
					continue; // unknown target
				}
				String resourcePath =
					zipFileName
						+ JAR_FILE_ENTRY_SEPARATOR
						+ type.getFullyQualifiedName().replace('.', '/')
						+ SUFFIX_STRING_class;

				this.resourcePaths.add(resourcePath);
				paths.put(jarPath, type);
			} else {
				// type is a project
				paths.put(type.getJavaProject().getProject().getFullPath(), type);
			}
		}
		this.enclosingProjectsAndJars = new IPath[paths.size()];
		int i = 0;
		for (Iterator iter = paths.keySet().iterator(); iter.hasNext();) {
			this.enclosingProjectsAndJars[i++] = (IPath) iter.next();
		}
	}
	/*
	 * Computes the paths of projects and jars that the hierarchy on the given type could contain.
	 * This is a super set of the project and jar paths once the hierarchy is computed.
	 */
	private IPath[] computeProjectsAndJars(IType type) throws JavaModelException {
		HashSet set = new HashSet();
		IPackageFragmentRoot root = (IPackageFragmentRoot)type.getPackageFragment().getParent();
		if (root.isArchive()) {
			// add the root
			set.add(root.getPath());
			// add all projects that reference this archive and their dependents
			IPath rootPath = root.getPath();
			IJavaModel model = JavaModelManager.getJavaModelManager().getJavaModel();
			IJavaProject[] projects = model.getJavaProjects();
			HashSet visited = new HashSet();
			for (int i = 0; i < projects.length; i++) {
				JavaProject project = (JavaProject) projects[i];
				IClasspathEntry entry = project.getClasspathEntryFor(rootPath);
				if (entry != null) {
					// add the project and its binary pkg fragment roots
					IPackageFragmentRoot[] roots = project.getAllPackageFragmentRoots();
					set.add(project.getPath());
					for (int k = 0; k < roots.length; k++) {
						IPackageFragmentRoot pkgFragmentRoot = roots[k];
						if (pkgFragmentRoot.getKind() == IPackageFragmentRoot.K_BINARY) {
							set.add(pkgFragmentRoot.getPath());
						}
					}
					// add the dependent projects
					computeDependents(project, set, visited);
				}
			}
		} else {
			// add all the project's pkg fragment roots
			IJavaProject project = (IJavaProject)root.getParent();
			IPackageFragmentRoot[] roots = project.getAllPackageFragmentRoots();
			for (int i = 0; i < roots.length; i++) {
				IPackageFragmentRoot pkgFragmentRoot = roots[i];
				if (pkgFragmentRoot.getKind() == IPackageFragmentRoot.K_BINARY) {
					set.add(pkgFragmentRoot.getPath());
				} else {
					set.add(pkgFragmentRoot.getParent().getPath());
				}
			}
			// add the dependent projects
			computeDependents(project, set, new HashSet());
		}
		IPath[] result = new IPath[set.size()];
		set.toArray(result);
		return result;
	}
	private void computeDependents(IJavaProject project, HashSet set, HashSet visited) {
		if (visited.contains(project)) return;
		visited.add(project);
		IProject[] dependents = project.getProject().getReferencingProjects();
		for (int i = 0; i < dependents.length; i++) {
			try {
				IJavaProject dependent = JavaCore.create(dependents[i]);
				IPackageFragmentRoot[] roots = dependent.getPackageFragmentRoots();
				set.add(dependent.getPath());
				for (int j = 0; j < roots.length; j++) {
					IPackageFragmentRoot pkgFragmentRoot = roots[j];
					if (pkgFragmentRoot.isArchive()) {
						set.add(pkgFragmentRoot.getPath());
					}
				}
				computeDependents(dependent, set, visited);
			} catch (JavaModelException e) {
				// project is not a java project
			}
		}
	}
	/* (non-Javadoc)
	 * @see IJavaSearchScope#encloses(String)
	 */
	public boolean encloses(String resourcePath) {
		if (this.hierarchy == null) {
			if (resourcePath.equals(this.focusPath)) {
				return true;
			} else {
				if (this.needsRefresh) {
					try {
						initialize();
					} catch (JavaModelException e) {
						return false;
					}
				} else {
					// the scope is used only to find enclosing projects and jars
					// clients is responsible for filtering out elements not in the hierarchy (see SearchEngine)
					return true;
				}
			}
		}
		if (this.needsRefresh) {
			try {
				refresh();
			} catch(JavaModelException e) {
				return false;
			}
		}
		int separatorIndex = resourcePath.indexOf(JAR_FILE_ENTRY_SEPARATOR);
		if (separatorIndex != -1) {
			return this.resourcePaths.contains(resourcePath);
		} else {
			for (int i = 0; i < this.elementCount; i++) {
				if (resourcePath.startsWith(this.elements[i].getFullPath().toString())) {
					return true;
				}
			}
		}
		return false;
	}
	/* (non-Javadoc)
	 * @see IJavaSearchScope#encloses(IJavaElement)
	 */
	public boolean encloses(IJavaElement element) {
		if (this.hierarchy == null) {
			if (this.focusType.equals(element.getAncestor(IJavaElement.TYPE))) {
				return true;
			} else {
				if (this.needsRefresh) {
					try {
						initialize();
					} catch (JavaModelException e) {
						return false;
					}
				} else {
					// the scope is used only to find enclosing projects and jars
					// clients is responsible for filtering out elements not in the hierarchy (see SearchEngine)
					return true;
				}
			}
		}
		if (this.needsRefresh) {
			try {
				refresh();
			} catch(JavaModelException e) {
				return false;
			}
		}
		IType type = null;
		if (element instanceof IType) {
			type = (IType) element;
		} else if (element instanceof IMember) {
			type = ((IMember) element).getDeclaringType();
		}
		if (type != null) {
			if (this.hierarchy.contains(type)) {
				return true;
			} else {
				// be flexible: look at original element (see bug 14106 Declarations in Hierarchy does not find declarations in hierarchy)
				IType original;
				if (!type.isBinary()
						&& (original = (IType)type.getPrimaryElement()) != null) {
					return this.hierarchy.contains(original);
				}
			}
		}
		return false;
	}
	/* (non-Javadoc)
	 * @see IJavaSearchScope#enclosingProjectsAndJars()
	 * @deprecated
	 */
	public IPath[] enclosingProjectsAndJars() {
		if (this.needsRefresh) {
			try {
				refresh();
			} catch(JavaModelException e) {
				return new IPath[0];
			}
		}
		return this.enclosingProjectsAndJars;
	}
//	protected void initialize() throws JavaModelException {
//		this.resourcePaths = new HashSet();
//		this.elements = new IResource[5];
//		this.elementCount = 0;
//		this.needsRefresh = false;
//		if (this.hierarchy == null) {
//			this.hierarchy = this.focusType.newTypeHierarchy(this.owner, null);
//		} else {
//			this.hierarchy.refresh(null);
//		}
//		buildResourceVector();
//	}
	/*
	 * @see AbstractSearchScope#processDelta(IJavaElementDelta)
	 */
	public void processDelta(IJavaElementDelta delta, int eventType) {
		if (this.needsRefresh) return;
		this.needsRefresh = this.hierarchy == null ? false : ((TypeHierarchy)this.hierarchy).isAffected(delta, eventType);
	}
	protected void refresh() throws JavaModelException {
		if (this.hierarchy != null) {
			initialize();
		}
	}
	public String toString() {
		return "WOHierarchyScope on " + ((JavaElement)this.focusType).toStringWithAncestors(); //$NON-NLS-1$
	}

	/**
	 * This is annoying, but we have to replace this ONE method, but we can't access our superclass variables, so just slurp the whole damn thing in.
	 * @throws JavaModelException
	 */
	protected void initialize() throws JavaModelException {
		this.resourcePaths = Collections.synchronizedSet(new HashSet<String>());
		this.elements = new IResource[5];
		this.elementCount = 0;
		this.needsRefresh = false;
		if (this.hierarchy == null) {
			this.hierarchy = SubTypeHierarchyCache.getTypeHierarchyInProject(this.focusType, this.javaProject, null);
		} else {
			this.hierarchy.refresh(null);
		}
		buildResourceVector();
	}
	
	protected static class WOHierarchyScopeCacheEntry {
		public IProject project;
		public int referenceCount;
		public Map<IType, WOHierarchyScope> scopes;
		
		public WOHierarchyScopeCacheEntry(IProject project) {
			scopes = new HashMap<IType, WOHierarchyScope>();
			this.project = project;
		}
		
		public synchronized WOHierarchyScope hierarchyScopeForType(IType type) throws JavaModelException {
			WOHierarchyScope scope = scopes.get(type);
			if (scope == null) {
				// System.out.println("WOHierarchyScope.WOHierarchyScopeCacheEntry.hierarchyScopeForType: cache MISS for " + type.getElementName() + " in " + project.getName());
				scope = new WOHierarchyScope(type, JavaCore.create(project));
				scopes.put(type, scope);
			}
			else {
				// System.out.println("WOHierarchyScope.WOHierarchyScopeCacheEntry.hierarchyScopeForType: cache hit for " + type.getElementName() + " in " + project.getName());
			}
			return scope;
		}
	}
	  
	private static Map<IProject, WOHierarchyScopeCacheEntry> _hierarchyScopeCache = new HashMap<IProject, WOHierarchyScopeCacheEntry>();
	  
	public static WOHierarchyScope hierarchyScope(IType superclassType, IProject project) throws JavaModelException {
		WOHierarchyScopeCacheEntry scopeCacheEntry;
		synchronized (_hierarchyScopeCache) {
			scopeCacheEntry = _hierarchyScopeCache.get(project);
		}
		if (scopeCacheEntry == null) {
			scopeCacheEntry = new WOHierarchyScopeCacheEntry(project);
		}
		return scopeCacheEntry.hierarchyScopeForType(superclassType);
	}
	  
	public static void clearCacheForProject(IProject project) {
		synchronized (_hierarchyScopeCache) {
			// System.out.println("WOHierarchyScope.clearCacheForProject: " + project.getName() + " clearing");
			_hierarchyScopeCache.remove(project);
		}
	}
	  
	public static WOHierarchyScopeCacheEntry incrementReferenceCountForProject(IProject project) {
		WOHierarchyScopeCacheEntry scopeCacheEntry;
		synchronized (_hierarchyScopeCache) {
			scopeCacheEntry = _hierarchyScopeCache.get(project);
			if (scopeCacheEntry == null) {
				scopeCacheEntry = new WOHierarchyScopeCacheEntry(project);
				_hierarchyScopeCache.put(project, scopeCacheEntry);
			}
			scopeCacheEntry.referenceCount ++;
			//System.out.println("WOHierarchyScope.incrementReferenceCountForProject: " + project.getName() + "=>" + scopeCacheEntry.referenceCount);
		}
		return scopeCacheEntry;
	}
	  
	public static void decrementReferenceCountForProject(IProject project) {
	  	synchronized (_hierarchyScopeCache) {
	  		WOHierarchyScopeCacheEntry scopeCacheEntry = _hierarchyScopeCache.get(project);
	  		if (scopeCacheEntry != null) {
	  			scopeCacheEntry.referenceCount --;
				//System.out.println("WOHierarchyScope.decrementReferenceCountForProject: " + project.getName() + "=>" + scopeCacheEntry.referenceCount);
	  			if (scopeCacheEntry.referenceCount <= 0) {
	  				clearCacheForProject(project);
	  			}
	  		}
	  	}
	}

}
