/*******************************************************************************
 * Copyright (c) 2011, 2012 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.wst.jsdt.web.core.internal;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.wst.common.componentcore.ComponentCore;
import org.eclipse.wst.common.componentcore.ModuleCoreNature;
import org.eclipse.wst.common.componentcore.resources.IVirtualFile;
import org.eclipse.wst.jsdt.core.IIncludePathAttribute;
import org.eclipse.wst.jsdt.core.IIncludePathEntry;
import org.eclipse.wst.jsdt.core.JavaScriptCore;
import org.eclipse.wst.jsdt.internal.core.ClasspathEntry;
import org.eclipse.wst.jsdt.internal.core.JavaProject;
import org.eclipse.wst.jsdt.internal.core.util.Messages;
import org.eclipse.wst.jsdt.web.core.internal.project.ModuleSourcePathProvider;
import org.eclipse.wst.sse.core.indexing.AbstractIndexManager;

/**
 * <p>This is an implementation of the {@link AbstractIndexManager} for the JavaScript Web core plugin.</p>
 * 
 * <p>Current Uses:
 * <ul>
 * <li>listen for .project changes so that JavaScript class paths can be updated
 * if the module core nature is added to a project</li>
 * </ul></p>
 * 
 * <p><b>NOTE:</b> If any other file resource change listening needs to take place in the future
 * in this plug-in it should be done here.</p>
 */
public class JSWebResourceEventManager extends AbstractIndexManager {
	/** the singleton instance of the {@link JSWebResourceEventManager} */
	private static JSWebResourceEventManager INSTANCE;
	
	/** the name of the ".project" file where natures are stored */
	private static final String DOT_PROJECT_FILE_NAME = ".project"; //$NON-NLS-1$
	
	/** the location to store state */
	private IPath fWorkingLocation;
	
	/**
	 * <p>Private constructor for the resource event manager</p>
	 */
	private JSWebResourceEventManager() {
		super(Messages.build_analyzingDeltas, Messages.build_analyzingDeltas,
				Messages.javamodel_initialization, Messages.manager_filesToIndex);
	}
	
	/**
	 * @return the singleton instance of the {@link JSWebResourceEventManager}
	 */
	public static JSWebResourceEventManager getDefault() {
		return INSTANCE != null ? INSTANCE : (INSTANCE = new JSWebResourceEventManager());
	}

	/**
	 * @see org.eclipse.wst.sse.core.indexing.AbstractIndexManager#isResourceToIndex(int, org.eclipse.core.runtime.IPath)
	 */
	protected boolean isResourceToIndex(int type, IPath path) {
		return 
			type == IResource.ROOT ||
			type == IResource.PROJECT || 
			(type == IResource.FILE && DOT_PROJECT_FILE_NAME.equals(path.lastSegment()));
	}

	/**
	 * @see org.eclipse.wst.sse.core.indexing.AbstractIndexManager#performAction(byte, byte, org.eclipse.core.resources.IResource, org.eclipse.core.runtime.IPath)
	 */
	protected void performAction(byte source, byte action, IResource resource,
			IPath movePath) {
		
		switch(action) {
			case(AbstractIndexManager.ACTION_ADD): {
				if(resource.getName().equals(DOT_PROJECT_FILE_NAME)) {
					updateClassPathEntries(resource.getProject());
				}
				break;
			}
		}

	}

	/**
	 * @see org.eclipse.wst.sse.core.indexing.AbstractIndexManager#getWorkingLocation()
	 */
	protected IPath getWorkingLocation() {
		if(this.fWorkingLocation == null) {
			//create path to working area
    		IPath workingLocation =
    			JsCorePlugin.getDefault().getStateLocation().append("JSWebResourceEventManager"); //$NON-NLS-1$

            // ensure that it exists on disk
            File folder = new File(workingLocation.toOSString());
    		if (!folder.isDirectory()) {
    			try {
    				folder.mkdir();
    			}
    			catch (SecurityException e) {
    				Logger.logException(this.getName() +
    						": Error while creating state location: " + folder + //$NON-NLS-1$
    						" This renders the index manager irrevocably broken for this workspace session", //$NON-NLS-1$
    						e);
    			}
    		}
    		
    		this.fWorkingLocation = workingLocation;
    	}
    	
        return this.fWorkingLocation;
	}
	
	/**
	 * <p>Updates the JavaScript class path entries for the given project if
	 * both the Module core and JavaScript natures are installed on that project.</p>
	 *
	 * @param project {@link IProject} to update the JavaScript class path entries for
	 */
	private static void updateClassPathEntries(IProject project) {
		try {
			/*
			 * if a JS project with Module Core nature, check if include path
			 * needs to be updated
			 */
			if (project.isAccessible() && project.hasNature(JavaScriptCore.NATURE_ID) && ModuleCoreNature.isFlexibleProject(project)) {
				JavaProject jsProject = (JavaProject) JavaScriptCore.create(project);

				IIncludePathEntry[] oldEntries = jsProject.getRawIncludepath();
				List updatedEntries = new ArrayList();
				boolean updateIncludePath = false;

				for (int oldEntry = 0; oldEntry < oldEntries.length; ++oldEntry) {
					IIncludePathAttribute[] entryAttributes = oldEntries[oldEntry].getExtraAttributes();

					boolean isProvidedEntry = false;
					for (int attribute = 0; attribute < entryAttributes.length; ++attribute) {
						isProvidedEntry = entryAttributes[attribute].getName().equals(ModuleSourcePathProvider.PROVIDER_ATTRIBUTE_KEY_NAME) && entryAttributes[attribute].getValue().equals(ModuleSourcePathProvider.PROVIDER_ATTRIBUTE_KEY_VALUE);
						updateIncludePath |= isProvidedEntry;
						if (isProvidedEntry) {
							/*
							 * create updated exclusion paths that are not
							 * relative to the parent entry
							 */
							IPath[] nonRelativeExclusionPaths = oldEntries[oldEntry].getExclusionPatterns();
							for (int i = 0; i < nonRelativeExclusionPaths.length; ++i) {
								nonRelativeExclusionPaths[i] = oldEntries[oldEntry].getPath().append(nonRelativeExclusionPaths[i]);
							}

							/*
							 * create updated inclusion paths that are not
							 * relative to the parent entry
							 */
							IPath[] nonRelativeInclusionPaths = oldEntries[oldEntry].getInclusionPatterns();
							for (int i = 0; i < nonRelativeInclusionPaths.length; ++i) {
								nonRelativeInclusionPaths[i] = oldEntries[oldEntry].getPath().append(nonRelativeInclusionPaths[i]);
							}

							IResource[] roots = getRoots(project);
							IIncludePathEntry[] resolvedEntries = jsProject.getResolvedClasspath();
							for (int root = 0; root < roots.length; ++root) {
								IPath rootPath = roots[root].getFullPath();
								
								// make sure we do not create a source entry that conflicts with an existing library
								boolean foundMatch = false;
								for(int k = 0; k < resolvedEntries.length; k++) {
									if(resolvedEntries[k].getPath().equals(rootPath)) {
										foundMatch = true;
										break;
									}
								}
								if(foundMatch)
									continue;

								/*
								 * find matching pre-existing exclusion
								 * patterns
								 */
								List exclusionPatterns = new ArrayList();
								for (int i = 0; i < nonRelativeExclusionPaths.length; ++i) {
									IPath parentRelativeExclusionPattern = PathUtils.makePatternRelativeToParent(nonRelativeExclusionPaths[i], rootPath);
									if (parentRelativeExclusionPattern != null) {
										exclusionPatterns.add(parentRelativeExclusionPattern);
									}
								}

								/*
								 * find matching pre-existing inclusion
								 * patterns
								 */
								List inclusionPatterns = new ArrayList();
								for (int i = 0; i < nonRelativeInclusionPaths.length; ++i) {
									IPath parentRelativeInclusionPattern = PathUtils.makePatternRelativeToParent(nonRelativeInclusionPaths[i], rootPath);
									if (parentRelativeInclusionPattern != null) {
										inclusionPatterns.add(parentRelativeInclusionPattern);
									}
								}

								// create new inclusion/exclusion rules
								IPath[] exclusionPaths = exclusionPatterns.isEmpty() ? ClasspathEntry.EXCLUDE_NONE : (IPath[]) exclusionPatterns.toArray(new IPath[exclusionPatterns.size()]);
								IPath[] inclusionPaths = inclusionPatterns.isEmpty() ? ClasspathEntry.INCLUDE_ALL : (IPath[]) inclusionPatterns.toArray(new IPath[inclusionPatterns.size()]);

								IIncludePathEntry newEntry = JavaScriptCore.newSourceEntry(rootPath, inclusionPaths, exclusionPaths, null);
								updatedEntries.add(newEntry);
							}
						}
					}
					if (!isProvidedEntry) {
						updatedEntries.add(oldEntries[oldEntry]);
					}
				}

				/*
				 * if found that a default source path was added, replace with
				 * module core determined path
				 */
				if (updateIncludePath) {
					// commit the updated include path
					jsProject.setRawIncludepath((IIncludePathEntry[]) updatedEntries.toArray(new IIncludePathEntry[updatedEntries.size()]), jsProject.getOutputLocation(), null);
				}
			}
		}
		catch (CoreException e) {
			Logger.logException("Error while updating JavaScript includepath", e); //$NON-NLS-1$
		}
	}
	
	/**
	 * <p>Uses module core to get the roots of the given project.</p>
	 *
	 * @param project find the module core roots for this {@link IProject}
	 * @return the module core roots for the given {@link IProject
	 */
	private static IResource[] getRoots(IProject project) {
		IVirtualFile root = ComponentCore.createFile(project, Path.ROOT);
		IResource[] underlyingResources = root.getUnderlyingResources();
		if (underlyingResources == null || underlyingResources.length == 0) {
			underlyingResources = new IResource[]{root.getUnderlyingResource()};
		}
		
		return underlyingResources;
	}
}