/*******************************************************************************
 * Copyright (c) 2003, 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.jst.server.core;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IAccessRule;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathContainer;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jst.server.core.internal.IMemento;
import org.eclipse.jst.server.core.internal.JavaServerPlugin;
import org.eclipse.jst.server.core.internal.RuntimeClasspathContainer;
import org.eclipse.jst.server.core.internal.Trace;
import org.eclipse.jst.server.core.internal.XMLMemento;
import org.eclipse.wst.server.core.IRuntime;
/**
 * A runtime classpath provider provides the classpath for a Java server runtime.
 * This provider is scoped by runtime type and may provide the classpath for multiple
 * runtime instances.
 * <p>
 * This abstract class is intended to be extended only by clients
 * to extend the <code>runtimeClasspathProviders</code> extension point.
 * </p>
 * <p>
 * <b>Provisional API:</b> This class/interface is part of an interim API that is still under development and expected to 
 * change significantly before reaching stability. It is being made available at this early stage to solicit feedback 
 * from pioneering adopters on the understanding that any code that uses this API will almost certainly be broken 
 * (repeatedly) as the API evolves.
 * </p>
 * 
 * @plannedfor 3.0
 */
public abstract class RuntimeClasspathProviderDelegate {
	protected class SourceAttachmentUpdate {
		String runtimeId;
		IPath entry;
		IPath sourceAttachmentPath;
		IPath sourceAttachmentRootPath;
		IClasspathAttribute[] attributes;
	}

	private List<SourceAttachmentUpdate> sourceAttachments;

	private String extensionId;

	private Map<String, IPath> runtimePathMap = new HashMap<String, IPath>();

	private Map<String, Integer> previousClasspath = new HashMap<String, Integer>();

	public RuntimeClasspathProviderDelegate() {
		// default constructor
	}

	/**
	 * Initializes this classpath provider with its life-long id.
	 * <p>
	 * This method is called by the framework.
	 * Clients should never call this method.
	 * </p>
	 * @param id the extension id
	 */
	public final void initialize(String id) {
		extensionId = id;
	}

	/**
	 * Resolves (creates the classpath entries for) the classpath container with
	 * the given runtime and the given classpath container id (returned from
	 * getClasspathEntryIds()). If the classpath container cannot be resolved
	 * (for instance, if the runtime does not exist), return null.
	 * 
	 * @param runtime the runtime to resolve the container for
	 * @return an array of classpath entries for the container, or null if the
	 *   container could not be resolved
	 * @deprecated use resolveClasspathContainer(IProject, IRuntime) instead
	 */
	public IClasspathEntry[] resolveClasspathContainer(IRuntime runtime) {
		return null;
	}

	/**
	 * Resolves (creates the classpath entries for) the classpath container with
	 * the given runtime and the given classpath container id (returned from
	 * getClasspathEntryIds()). If the classpath container cannot be resolved
	 * (for instance, if the runtime does not exist), return null.
	 * 
	 * @param project the project to resolve
	 * @param runtime the runtime to resolve the container for
	 * @return an array of classpath entries for the container, or null if the
	 *   container could not be resolved
	 */
	public IClasspathEntry[] resolveClasspathContainer(IProject project, IRuntime runtime) {
		return null;
	}

	/**
	 * Resolve the classpath container.
	 * 
	 * @param runtime a runtime
	 * @return a possibly empty array of classpath entries
	 * @deprecated should use resolveClasspathContainerImpl(IProject, IRuntime) instead
	 */
	public IClasspathEntry[] resolveClasspathContainerImpl(IRuntime runtime) {
		return resolveClasspathContainerImpl(null, runtime);
	}

	/**
	 * Resolve the classpath container.
	 * 
	 * @param project a project
	 * @param runtime a runtime
	 * @return a possibly empty array of classpath entries
	 */
	public IClasspathEntry[] resolveClasspathContainerImpl(IProject project, IRuntime runtime) {
		if (runtime == null)
			return new IClasspathEntry[0];
		runtimePathMap.put(runtime.getId(), runtime.getLocation());
		IClasspathEntry[] entries = resolveClasspathContainer(project, runtime);
		if (entries == null)
			entries = resolveClasspathContainer(runtime);
		
		if (entries == null)
			entries = new IClasspathEntry[0];
		
		if (sourceAttachments == null)
			load();
		
		int size = entries.length;
		int size2 = sourceAttachments.size();
		for (int i = 0; i < size; i++) {
			for (int j = 0; j < size2; j++) {
				SourceAttachmentUpdate sau = sourceAttachments.get(j);
				if (sau.runtimeId.equals(runtime.getId()) && sau.entry.equals(entries[i].getPath())) {
					entries[i] = JavaCore.newLibraryEntry(entries[i].getPath(), sau.sourceAttachmentPath, sau.sourceAttachmentRootPath, new IAccessRule[0], sau.attributes, false);
				}
			}
		}
		
		String key = project.getName() + "/" + runtime.getId();
		if (!previousClasspath.containsKey(key))
			previousClasspath.put(key, new Integer(entries.length));
		else {
			Integer previousEntries = previousClasspath.get(key);
			
			if ((previousEntries == null) || (previousEntries.intValue() != entries.length)) {
				Trace.trace(Trace.FINEST, "Classpath update: " + key + " " + entries);
				previousClasspath.put(key, new Integer(entries.length));
				
				IPath path = new Path(RuntimeClasspathContainer.SERVER_CONTAINER);
				path = path.append(extensionId).append(runtime.getId());
				try {
					IJavaProject javaProject = JavaCore.create(project);
					JavaCore.setClasspathContainer(path, new IJavaProject[] { javaProject },
							new IClasspathContainer[] { null }, new NullProgressMonitor());
				} catch (Exception e) {
					Trace.trace(Trace.WARNING, "Error updating classpath", e);
				}
			}
		}
		
		return entries;
	}

	/*
	 * Returns true if there are any changes in the runtime since the last time that the
	 * classpath was resolved which may affect the classpath, and false otherwise. This
	 * method is used to check projects when a runtime changes and automatically rebuild
	 * them if necessary.
	 * 
	 * @param runtime a runtime
	 * @return <code>true</code> if the classpath may change due to a change in the runtime,
	 *    and <code>false</code> if there are no changes
	 */
	public boolean hasRuntimeClasspathChanged(IRuntime runtime) {
		try {
			IPath path = runtimePathMap.get(runtime.getId());
			return (path != null && !path.equals(runtime.getLocation()));
		} catch (Exception e) {
			// ignore
		}
		return false;
	}

	private static void addJarFiles(File dir, List<IClasspathEntry> list, boolean includeSubdirectories) {
		int depth = 0;
		if (includeSubdirectories)
			depth = 2;
		addJarFiles(dir, list, depth);
	}

	private static void addJarFiles(File dir, List<IClasspathEntry> list, int depth) {
		if (dir == null)
			throw new IllegalArgumentException();
		
		File[] files = dir.listFiles();
		if (files != null) {
			for (File file : files) {
				if (file.isDirectory() && depth > 0) {
					addJarFiles(file, list, depth - 1);
				} else if (file.getAbsolutePath().endsWith(".jar") || file.getAbsolutePath().endsWith(".zip")) {
					IPath path = new Path(file.getAbsolutePath());
					list.add(JavaCore.newLibraryEntry(path, null, null));
				}
			}
		}
	}

	/**
	 * Add library entries to the given list for every jar file found in the
	 * given directory. Optionally search subdirectories as well.
	 * 
	 * @param list a list
	 * @param dir a directory
	 * @param includeSubdirectories <code>true</code> to include subdirectories, and
	 *    <code>false</code> otherwise
	 */
	protected static void addLibraryEntries(List<IClasspathEntry> list, File dir, boolean includeSubdirectories) {
		if (dir == null)
			throw new IllegalArgumentException();
		addJarFiles(dir, list, includeSubdirectories);
	}

	/**
	 * Request that the classpath container for the given runtime and id be updated
	 * with the given classpath container entries.
	 * 
	 * @param runtime a runtime
	 * @param entries an array of classpath entries
	 */
	public void requestClasspathContainerUpdate(IRuntime runtime, IClasspathEntry[] entries) {
		// default behaviour is to save the source path entries
		if (runtime == null || entries == null)
			return;
		
		// find the source attachments
		sourceAttachments = new ArrayList<SourceAttachmentUpdate>();
		
		for (IClasspathEntry entry : entries) {
			if (entry.getSourceAttachmentPath() != null || entry.getExtraAttributes() != null) {
				SourceAttachmentUpdate sau = new SourceAttachmentUpdate();
				sau.runtimeId = runtime.getId();
				sau.entry = entry.getPath();
				sau.sourceAttachmentPath = entry.getSourceAttachmentPath();
				sau.sourceAttachmentRootPath = entry.getSourceAttachmentRootPath();
				sau.attributes = entry.getExtraAttributes();
				sourceAttachments.add(sau);
			}
		}
		save();
	}

	/**
	 * Load source attachment info.
	 */
	private void load() {
		sourceAttachments = new ArrayList<SourceAttachmentUpdate>();
		
		String id = extensionId;
		String filename = JavaServerPlugin.getInstance().getStateLocation().append(id + ".xml").toOSString();
		if (!(new File(filename)).exists())
			return;
		
		try {
			IMemento memento = XMLMemento.loadMemento(filename);
			
			IMemento[] children = memento.getChildren("source-attachment");
			for (IMemento child : children) {
				try {
					SourceAttachmentUpdate sau = new SourceAttachmentUpdate();
					sau.runtimeId = child.getString("runtime-id");
					String temp = child.getString("entry");
					if (temp != null)
						sau.entry = new Path(temp);
					temp = child.getString("source-attachment-path");
					if (temp != null)
						sau.sourceAttachmentPath = new Path(temp);
					temp = child.getString("source-attachment-root-path");
					if (temp != null)
						sau.sourceAttachmentRootPath = new Path(temp);
					IMemento[] attrChildren = child.getChildren("attribute");
					if (attrChildren != null) {
						int size2 = attrChildren.length;
						sau.attributes = new IClasspathAttribute[size2];
						for (int j = 0; j < size2; j++) {
							String name = attrChildren[j].getString("name");
							String value = attrChildren[j].getString("value");
							sau.attributes[j] = JavaCore.newClasspathAttribute(name, value);
						}
					}
					sourceAttachments.add(sau);
				} catch (Exception e) {
					Trace.trace(Trace.WARNING, "Could not load source attachment: " + e);
				}
			}
		} catch (Exception e) {
			Trace.trace(Trace.WARNING, "Could not load source path info", e);
		}
	}

	/**
	 * Save source attachment info.
	 */
	private void save() {
		if (sourceAttachments == null)
			return;
		String id = extensionId;
		String filename = JavaServerPlugin.getInstance().getStateLocation().append(id + ".xml").toOSString();
		try {
			XMLMemento memento = XMLMemento.createWriteRoot("classpath");

			Iterator iterator = sourceAttachments.iterator();
			while (iterator.hasNext()) {
				SourceAttachmentUpdate sau = (SourceAttachmentUpdate) iterator.next();
				IMemento child = memento.createChild("source-attachment");
				child.putString("runtime-id", sau.runtimeId);
				if (sau.entry != null)
					child.putString("entry", sau.entry.toPortableString());
				if (sau.sourceAttachmentPath != null)
					child.putString("source-attachment-path", sau.sourceAttachmentPath.toPortableString());
				if (sau.sourceAttachmentRootPath != null)
					child.putString("source-attachment-root-path", sau.sourceAttachmentRootPath.toPortableString());
				if (sau.attributes != null) {
					for (IClasspathAttribute attr : sau.attributes) {
						IMemento attrChild = child.createChild("attribute");
						attrChild.putString("name", attr.getName());
						attrChild.putString("value", attr.getValue());
					}
				}
			}
			
			memento.saveToFile(filename);
		} catch (Exception e) {
			Trace.trace(Trace.SEVERE, "Error saving source path info", e);
		}
	}
}