/*******************************************************************************
 * Copyright (c) 2007, 2008 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.pde.api.tools.internal;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.ClassFileContainerVisitor;
import org.eclipse.pde.api.tools.internal.provisional.IClassFile;
import org.eclipse.pde.api.tools.internal.provisional.IClassFileContainer;
import org.eclipse.pde.api.tools.internal.util.Util;

/**
 * A class file container rooted at a directory in the file system.
 * 
 * @since 1.0.0
 */
public class DirectoryClassFileContainer implements IClassFileContainer {
	
	/**
	 * Root directory of the class file container
	 */
	private File fRoot;
	
	/**
	 * Origin of this class file container
	 */
	private String fOrigin;

	/**
	 * Map of package names to associated directory (file)
	 */
	private Map fPackages;
	
	/**
	 * Cache of package names
	 */
	private String[] fPackageNames;
	
	/**
	 * Implementation of a class file in the local file system.
	 * 
	 * @since 1.0.0
	 */
	class ClassFile extends AbstractClassFile implements Comparable {
		
		/**
		 * Associated file
		 */
		private File fFile;
		
		/**
		 * Qualified type name. Package is dot separated and types are $-separated.
		 */
		private String fTypeName;
		
		/**
		 * Constructs a class file on the given file
		 * @param file file
		 * @param qualified type name
		 */
		public ClassFile(File file, String typeName) {
			fFile = file;
			fTypeName = typeName;
		}

		/* (non-Javadoc)
		 * @see org.eclipse.pde.api.tools.model.component.IClassFile#getTypeName()
		 */
		public String getTypeName() {
			return fTypeName;
		}

		/* (non-Javadoc)
		 * @see java.lang.Comparable#compareTo(java.lang.Object)
		 */
		public int compareTo(Object o) {
			return fTypeName.compareTo(((ClassFile)o).fTypeName);
		}

		/* (non-Javadoc)
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		public boolean equals(Object obj) {
			if (obj instanceof ClassFile) {
				return ((ClassFile) obj).fTypeName.equals(this.fTypeName);
			}
			return false;
		}
		
		/* (non-Javadoc)
		 * @see java.lang.Object#hashCode()
		 */
		public int hashCode() {
			return this.fTypeName.hashCode();
		}

		/* (non-Javadoc)
		 * @see org.eclipse.pde.api.tools.model.component.IClassFile#getInputStream()
		 */
		public InputStream getInputStream() throws CoreException {
			try {
				return new FileInputStream(fFile);
			} catch (FileNotFoundException e) {
				abort("File not found", e); //$NON-NLS-1$
			}
			return null; // never reaches here
		}
	}	
	
	/**
	 * Constructs a class file container rooted at the specified path.
	 * 
	 * @param location absolute path in the local file system
	 * @param origin id of the component that creates this class file container
	 */
	public DirectoryClassFileContainer(String location, String origin) {
		this.fRoot = new File(location);
		this.fOrigin = origin;
	}

	/**
	 * Throws a core exception with the given message and underlying exception,
	 * if any.
	 * 
	 * @param message error message
	 * @param e underlying exception or <code>null</code>
	 * @throws CoreException
	 */
	private void abort(String message, Throwable e) throws CoreException {
		throw new CoreException(new Status(IStatus.ERROR, ApiPlugin.PLUGIN_ID, message, e));
	}	
	
	/* (non-Javadoc)
	 * @see org.eclipse.pde.api.tools.model.component.IClassFileContainer#accept(org.eclipse.pde.api.tools.model.component.ClassFileContainerVisitor)
	 */
	public void accept(ClassFileContainerVisitor visitor) throws CoreException {
		init();
		String[] packageNames = getPackageNames();
		for (int i = 0; i < packageNames.length; i++) {
			String pkg = packageNames[i];
			if (visitor.visitPackage(pkg)) {
				File dir = (File) fPackages.get(pkg);
				File[] files = dir.listFiles(new FileFilter() {
					public boolean accept(File file) {
						return file.isFile() && file.getName().endsWith(Util.DOT_CLASS_SUFFIX);
					}
				});
				if (files != null) {
					List classFiles = new ArrayList();
					for (int j = 0; j < files.length; j++) {
						File file = files[j];
						String name = file.getName();
						String typeName = name.substring(0, name.length() - 6);
						if (pkg.length() > 0) {
							typeName = pkg + "." + typeName; //$NON-NLS-1$
						}
						classFiles.add(new ClassFile(file, typeName));
					}
					Collections.sort(classFiles);
					Iterator cfIterator = classFiles.iterator();
					while (cfIterator.hasNext()) {
						IClassFile classFile = (IClassFile) cfIterator.next();
						visitor.visit(pkg, classFile);
						visitor.end(pkg, classFile);
					}
				}
			}
			visitor.endVisitPackage(pkg);
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.pde.api.tools.model.component.IClassFileContainer#close()
	 */
	public synchronized void close() throws CoreException {
		fPackages = null;
		fPackageNames = null;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.pde.api.tools.model.component.IClassFileContainer#findClassFile(java.lang.String)
	 */
	public IClassFile findClassFile(String qualifiedName) throws CoreException {
		init();
		int index = qualifiedName.lastIndexOf('.');
		String cfName = qualifiedName;
		String pkg = Util.DEFAULT_PACKAGE_NAME;
		if (index > 0) {
			pkg = qualifiedName.substring(0, index);
			cfName = qualifiedName.substring(index + 1);
		}
		File dir = (File) fPackages.get(pkg);
		if (dir != null) {
			File file = new File(dir, cfName + Util.DOT_CLASS_SUFFIX);
			if (file.exists()) {
				return new ClassFile(file, qualifiedName);
			}
		}
		return null;
	}

	
	/* (non-Javadoc)
	 * @see org.eclipse.pde.api.tools.model.component.IClassFileContainer#getPackageNames()
	 */
	public String[] getPackageNames() throws CoreException {
		init();
		if (fPackageNames == null) {
			List names = new ArrayList(fPackages.keySet());
			Collections.sort(names);
			fPackageNames = (String[]) names.toArray(new String[names.size()]);
		}
		return fPackageNames;
	}
	
	/**
	 * Builds cache of package names to directories
	 */
	private synchronized void init() {
		if (fPackages == null) {
			fPackages = new HashMap();
			processDirectory(Util.DEFAULT_PACKAGE_NAME, fRoot);
		}
	}
	
	/**
	 * Traverses a directory to determine if it has class files and
	 * then visits sub-directories.
	 * 
	 * @param packageName package name of directory being visited
	 * @param dir directory being visited
	 */
	private void processDirectory(String packageName, File dir) {
		File[] files = dir.listFiles();
		if (files != null) {
			boolean hasClassFiles = false;
			List dirs = new ArrayList();
			for (int i = 0; i < files.length; i++) {
				File file = files[i];
				if (file.isDirectory()) {
					dirs.add(file);
				} else if (!hasClassFiles) {
					if (file.getName().endsWith(Util.DOT_CLASS_SUFFIX)) {
						fPackages.put(packageName, dir);
						hasClassFiles = true;
					}
				}
			}
			Iterator iterator = dirs.iterator();
			while (iterator.hasNext()) {
				File child = (File)iterator.next();
				String nextName = null;
				if (packageName.length() == 0) {
					nextName = child.getName();
				} else {
					nextName = packageName + "." + child.getName(); //$NON-NLS-1$
				}
				processDirectory(nextName, child);
			}
		}
	}

	public IClassFile findClassFile(String qualifiedName, String id) throws CoreException {
		return findClassFile(qualifiedName);
	}

	public String getOrigin() {
		return this.fOrigin;
	}
}
