/*************************************************************************************
 * Copyright (c) 2008-2011 Red Hat, 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:
 *     JBoss by Red Hat - Initial implementation.
 ************************************************************************************/
package org.jboss.tools.common.jdt.core.buildpath;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Map;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
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.jdt.core.JavaModelException;
import org.eclipse.osgi.util.NLS;
import org.jboss.tools.common.jdt.core.JDTExtActivator;
import org.jboss.tools.common.jdt.core.Messages;
import org.jboss.tools.common.util.FileUtil;

public class MaterializeLibraryJob extends WorkspaceJob {

	private final IContainer libFolder;
	private final IJavaProject javaProject;
	private final Map<IPath, String> jars;
	private final IClasspathContainer containerToRemove;
	private final boolean keepSourceAttachments;

	public MaterializeLibraryJob(IJavaProject javaProject,
								 IClasspathContainer containerToMaterialize,
								 Map<IPath, String> jars, 
								 IContainer libFolder, 
								 boolean keepSourceAttachments) {
		super(Messages.Materialize_Library); 
		if (javaProject == null || javaProject.getProject() == null) {
			throw new IllegalArgumentException("Project must not be null");
		}
		if (containerToMaterialize == null) {
			throw new IllegalArgumentException("Container to materialize must not be null");
		}
		if (libFolder == null) {
			throw new IllegalArgumentException("Destination library folder must not be null");
		}
		this.javaProject = javaProject;
		this.libFolder = libFolder;
		this.containerToRemove = containerToMaterialize;
		this.jars = jars;
		this.keepSourceAttachments = keepSourceAttachments;
	}

	@Override
	public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException {

		final IPath rootPath = ResourcesPlugin.getWorkspace().getRoot().getLocation();
		
		IPath containerToRemovePath = containerToRemove.getPath();
		
		LinkedHashSet<IClasspathEntry> newCpes = null;
		
		//Create new Classpath entries
		if (jars != null && !jars.isEmpty()) {
			newCpes = copyClasspathEntries(monitor,	rootPath);	
		}
		
		// Remove the Classpath Library first
		removeClasspathContainer(javaProject, containerToRemovePath);

		// Then add the new Classpath entries
		if (newCpes != null && !newCpes.isEmpty()) {
			IClasspathEntry[] entries = new IClasspathEntry[newCpes.size()];
			newCpes.toArray(entries);
			addToClasspath(javaProject, entries, monitor);
		}

		//Finally execute post processors (needed to remove project nature, for ex.)
		for (ILibraryMaterializationPostProcessor processor : getPostProcessors()) {
			processor.execute(javaProject, containerToRemovePath, monitor);
		}

		//Refresh project
		IResource resourceToRefresh = libFolder.getProject();
		if (resourceToRefresh ==null) {
			resourceToRefresh = ResourcesPlugin.getWorkspace().getRoot();
		}
		resourceToRefresh.refreshLocal(IResource.DEPTH_INFINITE, monitor);

		return Status.OK_STATUS;
	}

	private LinkedHashSet<IClasspathEntry> copyClasspathEntries(
			IProgressMonitor monitor, final IPath rootPath)
			throws CoreException, JavaModelException {
		
		int jarSize = jars.size();
		
		monitor.beginTask(Messages.Materialize_Library, jarSize); 
		
		if (libFolder instanceof IFolder) {
			mkdirs((IFolder)libFolder, monitor);
		}

		IPath destination = libFolder.getLocation();

		LinkedHashSet<IClasspathEntry> newCpes = new LinkedHashSet<IClasspathEntry>(jarSize);

		IClasspathEntry[] cpEntries = containerToRemove.getClasspathEntries();

		for (IClasspathEntry entry : cpEntries) {
			if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
				
				IPath sourceFilePath = entry.getPath();
				String fileName = jars.get(sourceFilePath);
				if (fileName == null) {
					// Jar was not selected
					continue;
				}
				
				monitor.subTask(fileName);
				IPath destinationFilePath = destination.append(fileName);
				try {
					if ((javaProject.findPackageFragmentRoot(destinationFilePath) == null)// Not already defined
							&& copy(sourceFilePath, destinationFilePath)) {
						// TODO handle duplicate files -> rename?
						// FIXME use absolute / relative paths depending on the
						// location of the destination folder (in project /
						// elsewhere in the workspace)
						// FIXME need a more elegant way to get a relative path
						// with a leading /
						IPath relativePath = new Path("/").append(libFolder.getFullPath()).append(fileName); //$NON-NLS-1$
						IClasspathEntry newEntry = getNewClasspathEntry(entry, relativePath);
						newCpes.add(newEntry);
					}
				} catch (IOException e) {
					IStatus status = new Status(IStatus.ERROR,
							JDTExtActivator.PLUGIN_ID, NLS.bind(Messages.MaterializeLibraryJob_error_copying_file, fileName), e);
					throw new CoreException(status);
				}
			} else if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) {
				newCpes.add(entry);
			}
		}
		return newCpes;
	}

	private ILibraryMaterializationPostProcessor[] getPostProcessors() {
		ILibraryMaterializationPostProcessorFactory ppFactory = JDTExtActivator.getDefault()
																  .getLibraryMaterializationPostProcessorFactory();
		ILibraryMaterializationPostProcessor[] postProcessors = ppFactory.getLibraryMaterializationPostProcessors();
		return postProcessors;
	}

	private IClasspathEntry getNewClasspathEntry(IClasspathEntry entry,
			IPath destinationFilePath) throws CoreException {
		try {
			return JavaCore.newLibraryEntry(destinationFilePath,
					(keepSourceAttachments)?entry.getSourceAttachmentPath():null,
					(keepSourceAttachments)?entry.getSourceAttachmentRootPath():null,
					entry.getAccessRules(), entry.getExtraAttributes(),
					entry.isExported());
		} catch (Exception e) {
			IStatus status = new Status(IStatus.ERROR, JDTExtActivator.PLUGIN_ID, 
					NLS.bind(Messages.MaterializeLibraryJob_Error_creating_classpath_entry, e.getMessage()), e);
			throw new CoreException(status);
		}
	}

	private void addToClasspath(IJavaProject javaProject,
			IClasspathEntry[] newCpes, IProgressMonitor monitor)
			throws JavaModelException {
		if (newCpes.length > 0) {
			IClasspathEntry[] originalClasspathEntries = javaProject.getRawClasspath();
			IClasspathEntry[] newClasspath = new IClasspathEntry[originalClasspathEntries.length + newCpes.length];
			System.arraycopy(originalClasspathEntries, 0, newClasspath, 0,	originalClasspathEntries.length);
			System.arraycopy(newCpes, 0, newClasspath, originalClasspathEntries.length, newCpes.length);
			javaProject.setRawClasspath(newClasspath, monitor);
		}
	}

	private boolean copy(IPath sourceFilePath, IPath destinationFilePath)
			throws IOException {
		File sourcefile = sourceFilePath.toFile();
		if (sourcefile.isFile()) {
			File destinationFile = destinationFilePath.toFile();
			//Overwrite existing file
			FileUtil.copyFile(sourcefile, destinationFile);
			if (destinationFile.exists()) {
				return true;
			}
		}
		return false;
	}

	private void removeClasspathContainer(IJavaProject javaProject,
			IPath containerToRemovePath) throws JavaModelException {
		if (javaProject != null) {
			// remove classpatch container from JavaProject
			ArrayList<IClasspathEntry> newEntries = new ArrayList<IClasspathEntry>();
			for (IClasspathEntry entry : javaProject.getRawClasspath()) {
				if (!containerToRemovePath.equals(entry.getPath())) {
					newEntries.add(entry);
				}
			}
			javaProject.setRawClasspath(newEntries.toArray(new IClasspathEntry[newEntries.size()]),	null);
		}
	}

	private void mkdirs(final IFolder folder, IProgressMonitor monitor)
			throws CoreException {
		if (!folder.exists()) {
			if (folder.getParent() instanceof IFolder) {
				mkdirs((IFolder) folder.getParent(), monitor);
			}
			folder.create(true /* force */, true /* local */, monitor);
		} else {
			IContainer x = folder;
			while (x instanceof IFolder && x.isDerived()) {
				x.setDerived(false, monitor);
				x = x.getParent();
			}
		}
	}
}