/*******************************************************************************
 * Copyright (c) 2003, 2019 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 * IBM Corporation - initial API and implementation
 *******************************************************************************/
/*
 * Created on Mar 4, 2004
 *
 * To change the template for this generated file go to
 * Window&gt;Preferences&gt;Java&gt;Code Generation&gt;Code and Comments
 */
package org.eclipse.wst.common.internal.emfworkbench.integration;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
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.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.WrappedException;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.jem.internal.util.emf.workbench.EMFWorkbenchContextFactory;
import org.eclipse.jem.internal.util.emf.workbench.ProjectResourceSetImpl;
import org.eclipse.jem.util.emf.workbench.ProjectResourceSet;
import org.eclipse.jem.util.emf.workbench.ResourceSetWorkbenchSynchronizer;
import org.eclipse.jem.util.plugin.JEMUtilPlugin;
import org.eclipse.wst.common.internal.emf.resource.ReferencedResource;
import org.eclipse.wst.common.internal.emfworkbench.WorkbenchResourceHelper;

/**
 * @author schacher
 * 
 * To change the template for this generated type comment go to
 * Window&gt;Preferences&gt;Java&gt;Code Generation&gt;Code and Comments
 */
public class ResourceSetWorkbenchEditSynchronizer extends ResourceSetWorkbenchSynchronizer implements IResourceDeltaVisitor {
	private static final String CLASS_EXTENSION = "class"; //$NON-NLS-1$
	private static final String JAVA_EXTENSION = "java"; //$NON-NLS-1$
	private static final String JAVA_ARCHIVE = "jar"; //$NON-NLS-1$
	private Set recentlySavedFiles = new HashSet();
	private Map ignoredFilesCache = new HashMap();
	private class SavedFileKey {
		private WeakReference res;
		private IFile savedFile;
		public SavedFileKey(Resource res, IFile savedFile) {
			super();
			this.res = new WeakReference(res);
			this.savedFile = savedFile;
		}
		public Resource getRes() {
			return this.res == null ? null : (Resource)res.get();
		}
		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + getOuterType().hashCode();
			result = prime * result + ((getRes() == null) ? 0 : getRes().hashCode());
			result = prime * result + ((savedFile == null) ? 0 : savedFile.hashCode());
			return result;
		}
		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			SavedFileKey other = (SavedFileKey) obj;
			if (!getOuterType().equals(other.getOuterType()))
				return false;
			if (getRes() == null) {
				if (other.getRes() != null)
					return false;
			} else if (!getRes().equals(other.getRes()))
				return false;
			if (savedFile == null) {
				if (other.savedFile != null)
					return false;
			} else if (!savedFile.equals(other.savedFile))
				return false;
			return true;
		}
		private ResourceSetWorkbenchEditSynchronizer getOuterType() {
			return ResourceSetWorkbenchEditSynchronizer.this;
		}
	}

	/** The emf resources to be removed from the resource set as a result of a delta */
	protected List deferredRemoveResources = new ArrayList();
	protected List deferredUnloadResources = new ArrayList();
	protected List deferredLoadResources = new ArrayList();

	protected List autoloadResourcesURIs = new ArrayList();
	protected List autoloadResourcesExts = new ArrayList();


	/**
	 * @param aResourceSet
	 * @param aProject
	 */
	public ResourceSetWorkbenchEditSynchronizer(ResourceSet aResourceSet, IProject aProject) {
		super(aResourceSet, aProject);
	}
		
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.wst.common.internal.emfworkbench.ResourceSetWorkbenchSynchronizer#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
	 */
	@Override
	public void resourceChanged(IResourceChangeEvent event) {
		super.resourceChanged(event);
		try {
			acceptDelta(event);
			notifyExtendersIfNecessary();
			processDeferredResources();
		} catch (Exception e) {
			EMFWorkbenchEditPlugin.logError(e);
		} finally {
			deferredRemoveResources.clear();
			deferredUnloadResources.clear();
			deferredLoadResources.clear();
		}
	}

	protected void processDeferredRemovedResources() {
		Resource resource = null;
		for (int i = 0; i < deferredRemoveResources.size(); i++) {
			resource = (Resource) deferredRemoveResources.get(i);
			resourceSet.getResources().remove(resource);
			resource.unload();
		}
	}

	protected void processDeferredUnloadedResources() {
		Resource resource = null;
		for (int i = 0; i < deferredUnloadResources.size(); i++) {
			resource = (Resource) deferredUnloadResources.get(i);
			resource.unload();
		}
	}

	private void processDeferredLoadResources() {
		URI uri = null;
		for (int i = 0; i < deferredLoadResources.size(); i++) {
			uri = (URI) deferredLoadResources.get(i);
			try {
				resourceSet.getResource(uri, true);
			} catch (WrappedException ex) {
				EMFWorkbenchEditPlugin.logError(ex);
			}

		}
	}

	protected void acceptDelta(final IResourceChangeEvent event) {

		final IResourceDelta delta = event.getDelta();

		if (ResourcesPlugin.getWorkspace().isTreeLocked()) {
			primAcceptDelta(delta, event);
		}
		else {
			IWorkspaceRunnable runnable = new IWorkspaceRunnable() {
				@Override
				public void run(IProgressMonitor monitor) throws CoreException {
					primAcceptDelta(delta, event);
				}
			};
			try {
				ResourcesPlugin.getWorkspace().run(runnable, project, IWorkspace.AVOID_UPDATE, null);
			} catch (CoreException e) {
				EMFWorkbenchEditPlugin.logError(e);
			}
		}
	}

	private void primAcceptDelta(IResourceDelta delta, IResourceChangeEvent event) {
		if (delta != null) {
			try {
				currentProjectDelta = null;
				delta.accept(ResourceSetWorkbenchEditSynchronizer.this);
			} catch (Exception e) {
				EMFWorkbenchEditPlugin.logError(e);
			}
		}
	}

	/**
	 * The project is going away so we need to cleanup ourself and the ResourceSet. TODO Need to
	 * push up this code to ResourceSetWorkbenchSynchronizer in next release.
	 */
	@Override
	protected void release() {
		if (JEMUtilPlugin.isActivated()) {
			try {
				if (resourceSet instanceof ProjectResourceSet)
					((ProjectResourceSet) resourceSet).release();
			} finally {
				EMFWorkbenchContextFactory.INSTANCE.removeCachedProject(getProject());
				dispose();
			}
		}
	}

	private void processDeferredResources() {
		processDeferredRemovedResources();
		processDeferredUnloadedResources();
		processDeferredLoadResources();
	}

	@Override
	public boolean visit(IResourceDelta delta) {
		IResource resource = delta.getResource();
		// only respond to project changes
		if (resource != null) {
			if (resource.getType() == IResource.PROJECT) {
				IProject p = (IProject) resource;
				if (isInterrestedInProject(p)) {
					currentProjectDelta = delta;
					return true;
				}
				return false;
			}
			if (resource.getType() == IResource.FILE && isInterrestedInFile((IFile) resource)) {
				switch (delta.getKind()) {
					case IResourceDelta.REMOVED :
						removedResource((IFile) resource);
						break;
					case IResourceDelta.ADDED :
						addedResource((IFile) resource);
						break;
					case IResourceDelta.CHANGED :
						if ((delta.getFlags() & IResourceDelta.CONTENT) != 0)
							changedResource((IFile) resource);
						break;
					default :
						if ((delta.getFlags() & IResourceDelta.MOVED_FROM) != 0 || (delta.getFlags() & IResourceDelta.MOVED_TO) != 0)
							movedResource((IFile) resource);
						break;
				}
				return false;
			}
		}
		return true;
	}

	/**
	 * Queue up the <code>Resource</code> that corresponds to <code>aFile</code>, for removal
	 * from the cache of resources.
	 * 
	 * @post Return true if a <code>Resource</code> was queued up to be removed.
	 */
	protected boolean removedResource(IFile aFile) {
		return processResource(aFile, true);
	}

	/**
	 * Queue up the <code>Resource</code> that corresponds to <code>aFile</code>, for reload.
	 * 
	 * @post Return true if a <code>Resource</code> was queued up to be reloaded.
	 */
	protected boolean addedResource(IFile aFile) {
		boolean didProcess = false;
		List resources = getResources(aFile);
		for (Iterator iterator = resources.iterator(); iterator.hasNext();) {
			Resource resource = (Resource) iterator.next();

			if (resource != null) {
				if (recentlySavedFilesContains(resource)) {

					/*
					 * The IFile was just added to the workspace but we have a
					 * changed resource in memory. Need to decide if it should
					 * be unloaded.
					 */
					if (resource.isModified()) {
						if (WorkbenchResourceHelper.isReferencedResource(resource)) {
							ReferencedResource refRes = (ReferencedResource) resource;
							if (refRes.shouldForceRefresh()) {
								deferredUnloadResources.add(resource);
								didProcess = true;
							}
						}
					}
				} else {
					/* Unload if found and is not modified but inconsistent. */
					if (resource.isLoaded()) {
						if (WorkbenchResourceHelper.isReferencedResource(resource)) {
							if (!WorkbenchResourceHelper.isConsistent((ReferencedResource) resource)) {
								deferredUnloadResources.add(resource);
								didProcess = true;
							}
						} else {
							deferredUnloadResources.add(resource);
							didProcess = true;
						}
					} else {
						/* Process autoload resources on added file */
						URI uri = URI.createPlatformResourceURI(aFile.getFullPath().toString());
						if ((autoloadResourcesURIs.contains(uri)) || (autoloadResourcesExts.contains(aFile.getFileExtension()))) {
							deferredLoadResources.add(uri);
							didProcess = true;
						}
					}
				}
			}
		}
		return didProcess;
	}

	private synchronized boolean recentlySavedFilesContains(Resource resource) {
		for (Iterator iterator = recentlySavedFiles.iterator(); iterator.hasNext();) {
			SavedFileKey key = (SavedFileKey) iterator.next();
			if (key.getRes() != null && (key.getRes().getURI().equals(resource.getURI()) && key.getRes().getClass().equals(resource.getClass())))
				return true;
			}
		return false;
	}

	protected boolean processResource(IFile aFile, boolean isRemove) {
		List resources = getResources(aFile);
        for (Iterator iterator = resources.iterator(); iterator.hasNext();) {
			Resource resource = (Resource) iterator.next();
			if ((resource != null)) {
				if (recentlySavedFilesContains(resource)) {
					if (resource.isModified()) {
						if (WorkbenchResourceHelper.isReferencedResource(resource)) {
							ReferencedResource refRes = (ReferencedResource) resource;
							if (!refRes.shouldForceRefresh())
								continue; // Do not do anything
						} else
							continue;
					}
				}
				if (isRemove)
					deferredRemoveResources.add(resource);
				else if (resource.isLoaded()) {
					if (WorkbenchResourceHelper.isReferencedResource(resource)) {
						if (!WorkbenchResourceHelper.isConsistent((ReferencedResource) resource))
							deferredUnloadResources.add(resource);
					} else
						deferredUnloadResources.add(resource);
				}
			}
		}
		return false;
	}

	/**
	 * For now, do the same as if the <code>aFile</code> was removed.
	 */
	protected boolean movedResource(IFile aFile) {
		return removedResource(aFile);
	}

	/**
	 * The contents of <code>aFile</code> have changed in the Workbench and we may need to update
	 * our cached resources.
	 * 
	 * We will process this resource to be refreshed and not removed.
	 * 
	 * @post Return true if a <code>Resource</code> was actually removed.
	 */

	protected boolean changedResource(IFile aFile) {
		//Process resource as a refresh.
		return processResource(aFile, false);
	}

	protected Resource getResource(IFile aFile) {
		
		return resourceSet.getResource(URI.createPlatformResourceURI(aFile.getFullPath().toString()), false);
	}
	
	protected List getResources(IFile aFile) {

		List resources = new ArrayList();
		String aFileString = URI.decode(aFile.getFullPath().toString());
		IPath aFilePath = new Path(aFileString).removeFirstSegments(1);
		if (!aFilePath.isEmpty())
		{
			aFileString = aFilePath.toString();
		}
		if (aFileString.length() > 0)
		{
			List allResources = null;
			if (resourceSet instanceof ProjectResourceSetImpl) {
	            ProjectResourceSetImpl projResSet =(ProjectResourceSetImpl)resourceSet;
	            allResources = projResSet.getImmutableResources();
	        } else {
	            allResources = resourceSet.getResources();
	        }
			for (Iterator iterator = allResources.iterator(); iterator.hasNext();) {
				Resource res = (Resource) iterator.next();
				URI resURI = res.getURI();
				String resURIString = ""; //$NON-NLS-1$
				if (resURI.path() != null) {
					IPath resURIPath;
					if (WorkbenchResourceHelper.isPlatformResourceURI(resURI))
						resURIPath = new Path(URI.decode(resURI.path())).removeFirstSegments(2);
					else
						resURIPath = new Path(URI.decode(resURI.path())).removeFirstSegments(1);
					resURIString = resURIPath.toString();
				}
	
				if (resURIString.length() > 0)
				{
					if (aFileString.equals(resURIString))
					//if (!resURIString.equals("") && aFile.getFullPath().toString().indexOf(resURIString) != -1) //$NON-NLS-1$
						resources.add(res);
				}
			}
		}
		return resources;
	}


	/**
	 * This method should be called prior to writing to an IFile from a MOF resource.
	 */
	@Override
	public void preSave(IFile aFile) {
		if (aFile != null) {
			recentlySavedFilesAdd(aFile,null);
			ignoredFilesCache.remove(aFile);
		}
	}
	
	/**
	 * This method should be called prior to writing to an IFile from a MOF resource.
	 */
	public void preSave(IFile aFile, Resource res) {
		if (aFile != null) {
			recentlySavedFilesAdd(aFile, res);
			ignoredFilesCache.remove(aFile);
		}
	}

	private synchronized boolean recentlySavedFilesAdd(IFile file, Resource res) {
		return recentlySavedFiles.add(new SavedFileKey(res, file));
	}

	/**
	 * This method should be called after a preSave if the save fails
	 */
	public void removeFromRecentlySavedList(IFile aFile) {
		if (aFile != null) {
			recentlySavedFilesForceRemove(aFile);
			ignoredFilesCache.remove(aFile);
		}
	}

	private synchronized boolean recentlySavedFilesRemove(IFile file) {
		
		boolean removedFromList = false;
		for (Iterator iterator = recentlySavedFiles.iterator(); iterator.hasNext();) {
			SavedFileKey key = (SavedFileKey) iterator.next();
			if (key.savedFile != null && key.savedFile.equals(file)) {
				List resources = getResources(file);
				if (key.getRes() == null || resources.contains(key.getRes()) ) {
					iterator.remove();
					removedFromList = true;
				}
			}
		}
		return removedFromList;
	}
	private synchronized boolean recentlySavedFilesForceRemove(IFile file) {
		
		boolean removedFromList = false;
		for (Iterator iterator = recentlySavedFiles.iterator(); iterator.hasNext();) {
			SavedFileKey key = (SavedFileKey) iterator.next();
			if (key.savedFile != null && key.savedFile.equals(file)) {
					iterator.remove();
					removedFromList = true;
			}
		}
		return removedFromList;
	}

	/**
	 * This method should be called just after writing to an IFile from a MOF resource.
	 * 
	 * @deprecated No longer needs to be called.
	 */
	public void postSave(IFile aFile) {
		//TODO remove this method
	}

	/**
	 * Return <code>true</code> if <code>aProject</code> has the projectNatureID.
	 */
	protected boolean isInterrestedInProject(IProject aProject) {
		return aProject.equals(getProject());
	}

	/**
	 * Optimized not to be not interrested in files with an extension of .java or .class or if the
	 * file has just been saved by our own internal mechanism.
	 */
	protected boolean isInterrestedInFile(IFile aFile) {
		String extension = aFile.getFileExtension();
		if (CLASS_EXTENSION.equals(extension) || JAVA_EXTENSION.equals(extension) || JAVA_ARCHIVE.equals(extension))
			return false;
		if (recentlySavedFilesRemove(aFile)) {
				cacheIgnored(aFile);
				return false;
		}
		return !hasIgnored(aFile);
	}

	/**
	 * Return true if we have already ignored this <code>file</code> and that its modification
	 * stamp is the same as when we processed it.
	 * 
	 * @param file
	 * @return
	 */
	private boolean hasIgnored(IFile file) {
		Long cachedStamp = (Long) ignoredFilesCache.get(file);
		if (cachedStamp == null)
			return false;
		long stamp = WorkbenchResourceHelper.computeModificationStamp(file);
		return cachedStamp.longValue() == stamp;
	}

	/**
	 * Cache the modification stamp of the <code>file</code>.
	 * 
	 * @param file
	 */
	private void cacheIgnored(IFile file) {
		long stamp = WorkbenchResourceHelper.computeModificationStamp(file);
		ignoredFilesCache.put(file, new Long(stamp));
	}

	public void enableAutoload(URI uri) {
		URI normalized = resourceSet.getURIConverter().normalize(uri);
		autoloadResourcesURIs.add(normalized);
	}

	public void disableAutoload(URI uri) {
		URI normalized = resourceSet.getURIConverter().normalize(uri);
		autoloadResourcesURIs.remove(normalized);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.wst.common.internal.emfworkbench.ResourceSetWorkbenchSynchronizer#initialize()
	 */
	@Override
	protected void initialize() {
		getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.PRE_DELETE | IResourceChangeEvent.POST_CHANGE);
	}
	public void enableAutoload(String extension) {
		autoloadResourcesExts.add(extension);
		
	}
	public void disableAutoload(String extension) {
		autoloadResourcesExts.remove(extension);
	}

	@Override
	public void dispose() {
		super.dispose();
		currentProjectDelta = null;
		extenders = null;
	}

}
