/*******************************************************************************
 *  Copyright (c) 2007, 2017 aQute and others.
 *
 *  This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License 2.0
 *  which accompanies this distribution, and is available at
 *  https://www.eclipse.org/legal/epl-2.0/
 *
 *  SPDX-License-Identifier: EPL-2.0
 * 
 *  Contributors:
 *  aQute - initial implementation and ideas 
 *  IBM Corporation - initial adaptation to Equinox provisioning use
 *******************************************************************************/
package org.eclipse.equinox.internal.provisional.p2.directorywatcher;

import java.io.File;
import java.util.*;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.BundleContext;

public class DirectoryWatcher {
	private static final String DEL_EXT = ".del"; //$NON-NLS-1$

	public class WatcherThread extends Thread {

		private final long pollFrequency;
		private boolean done = false;

		public WatcherThread(long pollFrequency) {
			super("Directory Watcher"); //$NON-NLS-1$
			this.pollFrequency = pollFrequency;
		}

		@Override
		public void run() {
			do {
				try {
					poll();
					synchronized (this) {
						wait(pollFrequency);
					}
				} catch (InterruptedException e) {
					// ignore
				} catch (Throwable e) {
					log(Messages.error_main_loop, e);
					done = true;
				}
			} while (!done);
		}

		public synchronized void done() {
			done = true;
			notify();
		}
	}

	public final static String POLL = "eclipse.p2.directory.watcher.poll"; //$NON-NLS-1$
	public final static String DIR = "eclipse.p2.directory.watcher.dir"; //$NON-NLS-1$
	private static final long DEFAULT_POLL_FREQUENCY = 2000;

	public static void log(String string, Throwable e) {
		System.err.println(string + ": " + e); //$NON-NLS-1$
	}

	final File[] directories;

	long poll = 2000;
	private Set<DirectoryChangeListener> listeners = new HashSet<>();
	private HashSet<File> scannedFiles = new HashSet<>();
	private HashSet<File> removals;
	private Set<File> pendingDeletions;
	private WatcherThread watcher;

	public DirectoryWatcher(Map<String, String> properties, BundleContext context) {
		String dir = properties.get(DIR);
		if (dir == null)
			dir = "./load"; //$NON-NLS-1$

		File targetDirectory = new File(dir);
		targetDirectory.mkdirs();
		directories = new File[] {targetDirectory};
	}

	public DirectoryWatcher(File directory) {
		if (directory == null)
			throw new IllegalArgumentException(Messages.null_folder);

		this.directories = new File[] {directory};
	}

	public DirectoryWatcher(File[] directories) {
		if (directories == null)
			throw new IllegalArgumentException(Messages.null_folder);
		this.directories = directories;
	}

	public synchronized void addListener(DirectoryChangeListener listener) {
		listeners.add(listener);
	}

	public synchronized void removeListener(DirectoryChangeListener listener) {
		listeners.remove(listener);
	}

	public void start() {
		start(DEFAULT_POLL_FREQUENCY);
	}

	public synchronized void poll() {
		startPoll();
		scanDirectories();
		stopPoll();
	}

	public synchronized void start(final long pollFrequency) {
		if (watcher != null)
			throw new IllegalStateException(Messages.thread_started);

		watcher = new WatcherThread(pollFrequency);
		watcher.start();
	}

	public synchronized void stop() {
		if (watcher == null)
			throw new IllegalStateException(Messages.thread_not_started);

		watcher.done();
		watcher = null;
	}

	public File[] getDirectories() {
		return directories;
	}

	private void startPoll() {
		removals = scannedFiles;
		scannedFiles = new HashSet<>();
		pendingDeletions = new HashSet<>();
		for (DirectoryChangeListener listener : listeners)
			listener.startPoll();
	}

	private void scanDirectories() {
		for (File directory : directories) {
			File list[] = directory.listFiles();
			if (list == null)
				continue;
			for (File file : list) {
				// if this is a deletion marker then add to the list of pending deletions.
				if (file.getPath().endsWith(DEL_EXT)) {
					File target = new File(file.getPath().substring(0, file.getPath().length() - 4));
					removals.add(target);
					pendingDeletions.add(target);
				} else {
					// else remember that we saw the file and remove it from this list of files to be 
					// removed at the end.  Then notify all the listeners as needed.
					scannedFiles.add(file);
					removals.remove(file);
					for (DirectoryChangeListener listener : listeners) {
						if (isInterested(listener, file))
							processFile(file, listener);
					}
				}
			}
		}
	}

	private void stopPoll() {
		notifyRemovals();
		removals = scannedFiles;
		for (DirectoryChangeListener listener : listeners)
			listener.stopPoll();
		processPendingDeletions();
	}

	private boolean isInterested(DirectoryChangeListener listener, File file) {
		return listener.isInterested(file);
	}

	/**
	 * Notify the listeners of the files that have been deleted or marked for deletion.
	 */
	private void notifyRemovals() {
		Set<File> removed = removals;
		for (DirectoryChangeListener listener : listeners) {
			for (File file : removed) {
				if (isInterested(listener, file))
					listener.removed(file);
			}
		}
	}

	private void processFile(File file, DirectoryChangeListener listener) {
		try {
			Long oldTimestamp = listener.getSeenFile(file);
			if (oldTimestamp == null) {
				// The file is new
				listener.added(file);
			} else {
				// The file is not new but may have changed
				long lastModified = file.lastModified();
				if (oldTimestamp.longValue() != lastModified)
					listener.changed(file);
			}
		} catch (Exception e) {
			log(NLS.bind(Messages.error_processing, listener), e);
		}
	}

	/**
	 * Try to remove the files that have been marked for deletion.
	 */
	private void processPendingDeletions() {
		for (Iterator<File> iterator = pendingDeletions.iterator(); iterator.hasNext();) {
			File file = iterator.next();
			if (!file.exists() || file.delete())
				iterator.remove();
			new File(file.getPath() + DEL_EXT).delete();
		}
	}

}
