package org.rapidoid.io.watch;

import org.rapidoid.activity.AbstractLoopThread;
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.collection.Coll;
import org.rapidoid.commons.Err;
import org.rapidoid.log.Log;
import org.rapidoid.u.U;

import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.*;

/*
 * #%L
 * rapidoid-watch
 * %%
 * Copyright (C) 2014 - 2016 Nikolche Mihajlovski and contributors
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

@Authors("Nikolche Mihajlovski")
@Since("4.1.0")
public class WatcherThread extends AbstractLoopThread {

	private static final AtomicInteger idGen = new AtomicInteger();

	private final WatchService watchService;

	private final Map<WatchKey, Path> keys = U.map();

	private final FilesystemChangeListener onChange;

	private final boolean recursive;

	private final List<String> folders = Coll.synchronizedList();

	private final Set<String> watching = Coll.synchronizedSet();

	WatcherThread(FilesystemChangeListener change, Collection<String> targetFolders, boolean recursive) {
		this.onChange = change;
		this.recursive = recursive;

		for (String folder : targetFolders) {
			this.folders.add(new File(folder).getAbsolutePath());
		}

		setName("watcher" + idGen.incrementAndGet());

		try {
			watchService = FileSystems.getDefault().newWatchService();
		} catch (IOException e) {
			throw U.rte("Couldn't create a file system watch service!", e);
		}
	}

	private void init() {
		for (String folder : folders) {
			init(folder);
		}
	}

	private void init(String folder) {
		if (!watching.contains(folder) && new File(folder).exists()) {
			Log.debug("Watching folder for changes", "folder", folder, "recursive", recursive);

			Path dir = Paths.get(folder);
			if (recursive) {
				startWatchingTree(dir);
			} else {
				init(dir);
			}

			watching.add(folder);
		}
	}

	private void init(Path dir) {
		Log.info("Registering directory watch", "dir", dir);

		try {
			WatchKey key = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
			U.notNull(key, "watch key");

			keys.put(key, dir);
		} catch (IOException e) {
			Log.error("Couldn't register to watch for changes on: " + dir, e);
		}
	}

	private void startWatchingTree(final Path root) {
		try {
			Dir.traverse(root, new SimpleFileVisitor<Path>() {
				@Override
				public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
					init(dir);
					return FileVisitResult.CONTINUE;
				}
			});
		} catch (Exception e) {
			Log.warn("Couldn't register a watch for the directory tree", "dir", root);
		}
	}

	@Override
	protected void loop() {
		init();

		WatchKey key;
		try {
			key = watchService.take();

		} catch (InterruptedException | ClosedWatchServiceException x) {
			return;
		}

		Path dir = keys.get(key);
		if (dir == null) {
			return;
		}

		for (WatchEvent<?> event : key.pollEvents()) {
			try {
				processEvent(dir, event);
			} catch (Exception e) {
				Log.error("File system change processing error!", e);
			}
		}

		boolean isKeyValid = key.reset();
		if (!isKeyValid) {
			keys.remove(key);

			if (keys.isEmpty()) {
				if (!dir.toFile().exists()) {
					Log.warn("Cannot watch directory because it doesn't exist anymore", "dir", dir);
				} else {
					Log.error("Cannot watch directory due to unknown reason!", "dir", dir);
				}
			}

			watching.remove(dir.toFile().getAbsolutePath());
		}
	}

	private void processEvent(Path dir, WatchEvent<?> event) throws Exception {
		WatchEvent.Kind<?> kind = event.kind();

		if (ENTRY_CREATE.equals(kind)) {

			Path child = getChild(dir, event);

			if (recursive && Files.isDirectory(child, NOFOLLOW_LINKS)) {
				startWatchingTree(child);
			}

			onChange.created(fullNameOf(child));

		} else if (ENTRY_MODIFY.equals(kind)) {
			Path child = getChild(dir, event);

			onChange.modified(fullNameOf(child));

		} else if (ENTRY_DELETE.equals(kind)) {
			Path child = getChild(dir, event);

			onChange.deleted(fullNameOf(child));

		} else if (OVERFLOW.equals(kind)) {
			Log.warn("Received OVERFLOW event from the Watch service!");

		} else {
			throw Err.notExpected();
		}
	}

	private Path getChild(Path dir, WatchEvent<?> event) {
		WatchEvent<Path> ev = U.cast(event);
		Path path = ev.context();
		return dir.resolve(path);
	}

	private String fullNameOf(Path child) {
		return child.toAbsolutePath().toString();
	}

	public void cancel() {
		interrupt();

		Set<WatchKey> keysToCancel = U.set(keys.keySet());
		keys.clear();

		for (WatchKey key : keysToCancel) {
			key.cancel();
		}

		try {
			watchService.close();
		} catch (IOException e) {
			Log.error("Error occurred while closing a WatchService!", e);
		}
	}

}

@@ -2760,12 +2760,13 @@
 Log.
-info
+debug
 (%22Re

DEBUG level for the Watch registration logs.