/*******************************************************************************
 * Copyright (c) 2002, 2011 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 - Initial API and implementation
 *     James Blackburn (Broadcom Corp.) - ongoing development
 *******************************************************************************/
package org.eclipse.core.internal.resources.refresh.win32;

import java.io.File;
import java.util.*;
import org.eclipse.core.internal.refresh.RefreshManager;
import org.eclipse.core.internal.utils.Messages;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.refresh.IRefreshMonitor;
import org.eclipse.core.resources.refresh.IRefreshResult;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.Bundle;

/**
 * A monitor that works on Win32 platforms. Provides simple notification of
 * entire trees by reporting that the root of the tree has changed to depth
 * DEPTH_INFINITE.
 */
class Win32Monitor extends Job implements IRefreshMonitor {
	private static final long RESCHEDULE_DELAY = 1000;

	/**
	 * A ChainedHandle is a linked list of handles.
	 */
	protected abstract class ChainedHandle extends Handle {
		private ChainedHandle next;
		private ChainedHandle previous;

		public abstract boolean exists();

		public ChainedHandle getNext() {
			return next;
		}

		public ChainedHandle getPrevious() {
			return previous;
		}

		public void setNext(ChainedHandle next) {
			this.next = next;
		}

		public void setPrevious(ChainedHandle previous) {
			this.previous = previous;
		}
	}

	protected class FileHandle extends ChainedHandle {
		private File file;

		public FileHandle(File file) {
			this.file = file;
		}

		public boolean exists() {
			return file.exists();
		}

		public void handleNotification() {
			if (!isOpen())
				return;
			ChainedHandle next = getNext();
			if (next != null) {
				if (next.isOpen()) {
					if (!next.exists()) {
						if (next instanceof LinkedResourceHandle) {
							next.close();
							LinkedResourceHandle linkedResourceHandle = (LinkedResourceHandle) next;
							linkedResourceHandle.postRefreshRequest();
						} else {
							next.close();
						}
						ChainedHandle previous = getPrevious();
						if (previous != null)
							previous.open();
					}
				} else {
					next.open();
					if (next.isOpen()) {
						Handle previous = getPrevious();
						previous.close();
						if (next instanceof LinkedResourceHandle)
							((LinkedResourceHandle) next).postRefreshRequest();
					}
				}
			}
			findNextChange();
		}

		public void open() {
			if (!isOpen()) {
				Handle next = getNext();
				if (next != null && next.isOpen()) {
					openHandleOn(file);
				} else {
					if (exists()) {
						openHandleOn(file);
					}
					Handle previous = getPrevious();
					if (previous != null) {
						previous.open();
					}
				}
			}
		}
	}

	protected abstract class Handle {
		protected long handleValue;

		public Handle() {
			handleValue = Win32Natives.INVALID_HANDLE_VALUE;
		}

		public void close() {
			if (isOpen()) {
				if (!Win32Natives.FindCloseChangeNotification(handleValue)) {
					int error = Win32Natives.GetLastError();
					if (error != Win32Natives.ERROR_INVALID_HANDLE)
						addException(NLS.bind(Messages.WM_errCloseHandle, Integer.toString(error)));
				}
				if (RefreshManager.DEBUG)
					System.out.println(DEBUG_PREFIX + "removed handle: " + handleValue); //$NON-NLS-1$
				handleValue = Win32Natives.INVALID_HANDLE_VALUE;
			}
		}

		private long createHandleValue(String path, boolean monitorSubtree, int flags) {
			long handle = Win32Natives.FindFirstChangeNotification(path, monitorSubtree, flags);
			if (handle == Win32Natives.INVALID_HANDLE_VALUE) {
				int error = Win32Natives.GetLastError();
				addException(NLS.bind(Messages.WM_errCreateHandle, path, Integer.toString(error)));
			}
			return handle;
		}

		public void destroy() {
			close();
		}

		protected void findNextChange() {
			if (!Win32Natives.FindNextChangeNotification(handleValue)) {
				int error = Win32Natives.GetLastError();
				if (error != Win32Natives.ERROR_INVALID_HANDLE && error != Win32Natives.ERROR_SUCCESS) {
					addException(NLS.bind(Messages.WM_errFindChange, Integer.toString(error)));
				}
				removeHandle(this);
			}
		}

		public long getHandleValue() {
			return handleValue;
		}

		public abstract void handleNotification();

		public boolean isOpen() {
			return handleValue != Win32Natives.INVALID_HANDLE_VALUE;
		}

		public abstract void open();

		protected void openHandleOn(File file) {
			openHandleOn(file.getAbsolutePath(), false);
		}

		protected void openHandleOn(IResource resource) {
			openHandleOn(resource.getLocation().toOSString(), true);
		}

		private void openHandleOn(String path, boolean subtree) {
			setHandleValue(createHandleValue(path, subtree, Win32Natives.FILE_NOTIFY_CHANGE_FILE_NAME | Win32Natives.FILE_NOTIFY_CHANGE_DIR_NAME | Win32Natives.FILE_NOTIFY_CHANGE_LAST_WRITE | Win32Natives.FILE_NOTIFY_CHANGE_SIZE));
			if (isOpen()) {
				fHandleValueToHandle.put(new Long(getHandleValue()), this);
				setHandleValueArrays(createHandleArrays());
			} else {
				close();
			}
		}

		protected void postRefreshRequest(IResource resource) {
			//native callback occurs even if resource was changed within workspace
			if (!resource.isSynchronized(IResource.DEPTH_INFINITE))
				refreshResult.refresh(resource);
		}

		public void setHandleValue(long handleValue) {
			this.handleValue = handleValue;
		}
	}

	protected class LinkedResourceHandle extends ChainedHandle {
		private List<FileHandle> fileHandleChain;
		private IResource resource;

		/**
		 * @param resource
		 */
		public LinkedResourceHandle(IResource resource) {
			this.resource = resource;
			createFileHandleChain();
		}

		protected void createFileHandleChain() {
			fileHandleChain = new ArrayList<FileHandle>(1);
			File file = new File(resource.getLocation().toOSString());
			file = file.getParentFile();
			while (file != null) {
				fileHandleChain.add(0, new FileHandle(file));
				file = file.getParentFile();
			}
			int size = fileHandleChain.size();
			for (int i = 0; i < size; i++) {
				ChainedHandle handle = fileHandleChain.get(i);
				handle.setPrevious((i > 0) ? fileHandleChain.get(i - 1) : null);
				handle.setNext((i + 1 < size) ? fileHandleChain.get(i + 1) : this);
			}
			setPrevious((size > 0) ? fileHandleChain.get(size - 1) : null);
		}

		public void destroy() {
			super.destroy();
			for (Iterator<FileHandle> i = fileHandleChain.iterator(); i.hasNext();) {
				Handle handle = i.next();
				handle.destroy();
			}
		}

		public boolean exists() {
			IPath location = resource.getLocation();
			return location == null ? false : location.toFile().exists();
		}

		public void handleNotification() {
			if (isOpen()) {
				postRefreshRequest(resource);
				findNextChange();
			}
		}

		public void open() {
			if (!isOpen()) {
				if (exists()) {
					openHandleOn(resource);
				}
				FileHandle handle = (FileHandle) getPrevious();
				if (handle != null && !handle.isOpen()) {
					handle.open();
				}
			}
		}

		public void postRefreshRequest() {
			postRefreshRequest(resource);
		}
	}

	protected class ResourceHandle extends Handle {
		private IResource resource;

		public ResourceHandle(IResource resource) {
			super();
			this.resource = resource;
		}

		public IResource getResource() {
			return resource;
		}

		public void handleNotification() {
			if (isOpen()) {
				postRefreshRequest(resource);
				findNextChange();
			}
		}

		public void open() {
			if (!isOpen()) {
				openHandleOn(resource);
			}
		}
	}

	private static final String DEBUG_PREFIX = "Win32RefreshMonitor: "; //$NON-NLS-1$
	private static final int WAIT_FOR_MULTIPLE_OBJECTS_TIMEOUT = 300;
	/**
	 * Any errors that have occurred
	 */
	protected MultiStatus errors;
	/**
	 * Arrays of handles, split evenly when the number of handles is larger
	 * than Win32Natives.MAXIMUM_WAIT_OBJECTS
	 */
	protected long[][] fHandleValueArrays;
	/**
	 * Mapping of handles (java.lang.Long) to absolute paths
	 * (java.lang.String).
	 */
	protected Map<Long, Handle> fHandleValueToHandle;
	protected IRefreshResult refreshResult;

	/*
	 * Creates a new monitor. @param result A result that will receive refresh
	 * callbacks and error notifications
	 */
	public Win32Monitor(IRefreshResult result) {
		super(Messages.WM_jobName);
		this.refreshResult = result;
		setPriority(Job.DECORATE);
		setSystem(true);
		fHandleValueToHandle = new HashMap<Long, Handle>(1);
		setHandleValueArrays(createHandleArrays());
	}

	/**
	 * Logs an exception
	 */
	protected synchronized void addException(String message) {
		if (errors == null) {
			String msg = Messages.WM_errors;
			errors = new MultiStatus(ResourcesPlugin.PI_RESOURCES, 1, msg, null);
		}
		errors.add(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, 1, message, null));
	}

	/*
	 * Splits the given array into arrays of length no greater than <code> max
	 * </code> . The lengths of the sub arrays differ in size by no more than
	 * one element. <p> Examples: <ul><li> If an array of size 11 is split
	 * with a max of 4, the resulting arrays are of size 4, 4, and 3. </li>
	 * <li> If an array of size 18 is split with a max of 5, the resulting
	 * arrays are of size 5, 5, 4, and 4. </li></ul>
	 */
	private long[][] balancedSplit(final long[] array, final int max) {
		int elementCount = array.length;
		// want to handle [1, max] rather than [0, max)
		int subArrayCount = ((elementCount - 1) / max) + 1;
		int subArrayBaseLength = elementCount / subArrayCount;
		int overflow = elementCount % subArrayCount;
		long[][] result = new long[subArrayCount][];
		int count = 0;
		for (int i = 0; i < subArrayCount; i++) {
			int subArrayLength = subArrayBaseLength + (overflow-- > 0 ? 1 : 0);
			long[] subArray = new long[subArrayLength];
			for (int j = 0; j < subArrayLength; j++) {
				subArray[j] = array[count++];
			}
			result[i] = subArray;
		}
		return result;
	}

	private Handle createHandle(IResource resource) {
		if (resource.isLinked())
			return new LinkedResourceHandle(resource);
		return new ResourceHandle(resource);
	}

	/*
	 * Since the Win32Natives.WaitForMultipleObjects(...) method cannot accept
	 * more than a certain number of objects, we are forced to split the array
	 * of objects to monitor and monitor each one individually. <p> This method
	 * splits the list of handles into arrays no larger than
	 * Win32Natives.MAXIMUM_WAIT_OBJECTS. The arrays are balanced so that they
	 * differ in size by no more than one element.
	 */
	protected long[][] createHandleArrays() {
		long[] handles;
		// synchronized: in order to protect the map during iteration
		synchronized (fHandleValueToHandle) {
			Set<Long> keys = fHandleValueToHandle.keySet();
			int size = keys.size();
			if (size == 0) {
				return new long[0][0];
			}
			handles = new long[size];
			int count = 0;
			for (Iterator<Long> i = keys.iterator(); i.hasNext();) {
				handles[count++] = i.next().longValue();
			}
		}
		return balancedSplit(handles, Win32Natives.MAXIMUM_WAIT_OBJECTS);
	}

	private Handle getHandle(IResource resource) {
		if (resource == null) {
			return null;
		}
		// synchronized: in order to protect the map during iteration
		synchronized (fHandleValueToHandle) {
			for (Iterator<Handle> i = fHandleValueToHandle.values().iterator(); i.hasNext();) {
				Handle handle = i.next();
				if (handle instanceof ResourceHandle) {
					ResourceHandle resourceHandle = (ResourceHandle) handle;
					if (resourceHandle.getResource().equals(resource)) {
						return handle;
					}
				}
			}
		}
		return null;
	}

	/*
	 * Answers arrays of handles. The handles are split evenly when the number
	 * of handles becomes larger than Win32Natives.MAXIMUM_WAIT_OBJECTS.
	 * @return long[][]
	 */
	private long[][] getHandleValueArrays() {
		return fHandleValueArrays;
	}

	/**
	 * Adds a resource to be monitored by this native monitor
	 */
	public boolean monitor(IResource resource) {
		IPath location = resource.getLocation();
		if (location == null) {
			// cannot monitor remotely managed containers
			return false;
		}
		Handle handle = createHandle(resource);
		// synchronized: handle creation must be atomic
		synchronized (this) {
			handle.open();
		}
		if (!handle.isOpen()) {
			//ignore errors if we can't even create a handle on the resource
			//it will fall back to polling anyway
			errors = null;
			return false;
		}
		//make sure the job is running
		schedule(RESCHEDULE_DELAY);
		if (RefreshManager.DEBUG)
			System.out.println(DEBUG_PREFIX + " added monitor for: " + resource); //$NON-NLS-1$
		return true;
	}

	/**
	 * Removes the handle from the <code>fHandleValueToHandle</code> map.
	 * 
	 * @param handle
	 *                  a handle, not <code>null</code>
	 */
	protected void removeHandle(Handle handle) {
		List<Handle> handles = new ArrayList<Handle>(1);
		handles.add(handle);
		removeHandles(handles);
	}

	/**
	 * Removes all of the handles in the given collection from the <code>fHandleValueToHandle</code>
	 * map. If collections from the <code>fHandleValueToHandle</code> map are
	 * used, copy them before passing them in as this method modifies the
	 * <code>fHandleValueToHandle</code> map.
	 * 
	 * @param handles
	 *                  a collection of handles, not <code>null</code>
	 */
	private void removeHandles(Collection<Handle> handles) {
		// synchronized: protect the array, removal must be atomic
		synchronized (this) {
			for (Iterator<Handle> i = handles.iterator(); i.hasNext();) {
				Handle handle = i.next();
				fHandleValueToHandle.remove(new Long(handle.getHandleValue()));
				handle.destroy();
			}
			setHandleValueArrays(createHandleArrays());
		}
	}

	/*
	 * @see java.lang.Runnable#run()
	 */
	protected IStatus run(IProgressMonitor monitor) {
		long start = -System.currentTimeMillis();
		if (RefreshManager.DEBUG)
			System.out.println(DEBUG_PREFIX + "job started."); //$NON-NLS-1$
		try {
			long[][] handleArrays = getHandleValueArrays();
			monitor.beginTask(Messages.WM_beginTask, handleArrays.length);
			// If changes occur to the list of handles,
			// ignore them until the next time through the loop.
			for (int i = 0, length = handleArrays.length; i < length; i++) {
				if (monitor.isCanceled())
					return Status.CANCEL_STATUS;
				waitForNotification(handleArrays[i]);
				monitor.worked(1);
			}
		} finally {
			monitor.done();
			start += System.currentTimeMillis();
			if (RefreshManager.DEBUG)
				System.out.println(DEBUG_PREFIX + "job finished in: " + start + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
		}
		//always reschedule the job - so it will come back after errors or cancelation
		//make sure it doesn't hog more that 5% of CPU
		long delay = Math.max(RESCHEDULE_DELAY, start * 30);
		if (RefreshManager.DEBUG)
			System.out.println(DEBUG_PREFIX + "rescheduling in: " + delay / 1000 + " seconds"); //$NON-NLS-1$ //$NON-NLS-2$
		final Bundle bundle = Platform.getBundle(ResourcesPlugin.PI_RESOURCES);
		//if the bundle is null then the framework has shutdown - just bail out completely (bug 98219)
		if (bundle == null)
			return Status.OK_STATUS;
		//don't reschedule the job if the resources plugin has been shut down
		if (bundle.getState() == Bundle.ACTIVE)
			schedule(delay);
		MultiStatus result = errors;
		errors = null;
		//just log native refresh failures
		if (result != null && !result.isOK())
			ResourcesPlugin.getPlugin().getLog().log(result);
		return Status.OK_STATUS;
	}

	protected void setHandleValueArrays(long[][] arrays) {
		fHandleValueArrays = arrays;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.runtime.jobs.Job#shouldRun()
	 */
	public boolean shouldRun() {
		return !fHandleValueToHandle.isEmpty();
	}

	/*
	 * @see org.eclipse.core.resources.refresh.IRefreshMonitor#unmonitor(IContainer)
	 */
	public void unmonitor(IResource resource) {
		if (resource == null) {
			// resource == null means stop monitoring all resources
			synchronized (fHandleValueToHandle) {
				removeHandles(new ArrayList<Handle>(fHandleValueToHandle.values()));
			}
		} else {
			Handle handle = getHandle(resource);
			if (handle != null)
				removeHandle(handle);
		}
		//stop the job if there are no more handles
		if (fHandleValueToHandle.isEmpty())
			cancel();
	}

	/**
	 * Performs the native call to wait for notification on one of the given
	 * handles.
	 * 
	 * @param handleValues
	 *                  an array of handles, it must contain no duplicates.
	 */
	private void waitForNotification(long[] handleValues) {
		int handleCount = handleValues.length;
		int index = Win32Natives.WaitForMultipleObjects(handleCount, handleValues, false, WAIT_FOR_MULTIPLE_OBJECTS_TIMEOUT);
		if (index == Win32Natives.WAIT_TIMEOUT) {
			// nothing happened.
			return;
		}
		if (index == Win32Natives.WAIT_FAILED) {
			// we ran into a problem
			int error = Win32Natives.GetLastError();
			if (error != Win32Natives.ERROR_INVALID_HANDLE && error != Win32Natives.ERROR_SUCCESS) {
				addException(NLS.bind(Messages.WM_nativeErr, Integer.toString(error)));
				refreshResult.monitorFailed(this, null);
			}
			return;
		}
		// a change occurred
		// WaitForMultipleObjects returns WAIT_OBJECT_0 + index
		index -= Win32Natives.WAIT_OBJECT_0;
		Handle handle = fHandleValueToHandle.get(new Long(handleValues[index]));
		if (handle != null)
			handle.handleNotification();
	}
}
