/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */
package org.apache.wicket.core.util.watch;

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;

import org.apache.wicket.Application;
import org.apache.wicket.ThreadContext;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.util.io.IOUtils;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.thread.ICode;
import org.apache.wicket.util.thread.Task;
import java.time.Duration;
import org.apache.wicket.util.watch.ModificationWatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An extension of ModificationWatcher that removes the NotFound entries from
 * the MarkupCache for newly created files.
 *
 * By default MarkupCache registers Markup.NO_MARKUP value for each requested but
 * not found markup file. Later when the user creates the markup file the MarkupCache
 * should be notified.
 *
 * @since 7.0.0
 */
public class Nio2ModificationWatcher extends ModificationWatcher
{
	private static final Logger LOG = LoggerFactory.getLogger(Nio2ModificationWatcher.class);

	private final WatchService watchService;
	private final Application application;

	/** the <code>Task</code> to run */
	private Task task;

	/**
	 * Constructor.
	 *
	 * @param application
	 *              The application that manages the caches
	 * @param pollFrequency
	 *              How often to check on <code>IModifiable</code>s
	 */
	public Nio2ModificationWatcher(final Application application, Duration pollFrequency)
	{
		try
		{
			this.application = application;

			this.watchService = FileSystems.getDefault().newWatchService();
			registerWatchables(watchService);

			start(pollFrequency);

		} catch (IOException iox)
		{
			throw new WicketRuntimeException("Cannot get the watch service", iox);
		}
	}

	@Override
	public void start(final Duration pollFrequency)
	{
		// Construct task with the given polling frequency
		task = new Task("Wicket-ModificationWatcher-NIO2");

		task.run(pollFrequency, new ICode() {
			@Override
			public void run(final Logger log)
			{
				checkCreated(log);
				checkModified();
			}
		});
	}

	/**
	 * Checks for newly created files and folders.
	 * New folders are registered to be watched.
	 * New files are removed from the MarkupCache because there could be
	 * {@link org.apache.wicket.markup.Markup#NO_MARKUP} (Not Found) entries for them already.
	 * @param log
	 *              a logger that can be used to log the events
	 */
	protected void checkCreated(Logger log)
	{
		WatchKey watchKey = watchService.poll();
		if (watchKey != null)
		{
			List<WatchEvent<?>> events = watchKey.pollEvents();
			for (WatchEvent<?> event : events)
			{
				WatchEvent.Kind<?> eventKind = event.kind();
				Path eventPath = (Path) event.context();

				if (eventKind == ENTRY_CREATE)
				{
					entryCreated(eventPath, log);
				}
				else if (eventKind == ENTRY_DELETE)
				{
					entryDeleted(eventPath, log);
				}
				else if (eventKind == ENTRY_MODIFY)
				{
					entryModified(eventPath, log);
				}
			}

			watchKey.reset();
		}
	}

	/**
	 * A callback method called when a new Path entry is modified
	 *
	 * @param path
	 *              the modified path
	 * @param log
	 *              a logger that can be used to log the events
	 */
	protected void entryModified(Path path, Logger log)
	{
	}

	/**
	 * A callback method called when a new Path entry is deleted
	 *
	 * @param path
	 *              the deleted path
	 * @param log
	 *              a logger that can be used to log the events
	 */
	protected void entryDeleted(Path path, Logger log)
	{
	}

	/**
	 * A callback method called when a new Path entry is created
	 *
	 * @param path
	 *              the new path entry
	 * @param log
	 *              a logger that can be used to log the events
	 */
	protected void entryCreated(Path path, Logger log)
	{
		if (Files.isDirectory(path))
		{
			try
			{
				// a directory is created. register it for notifications
				register(path, watchService);
			} catch (IOException iox)
			{
				log.warn("Cannot register folder '" + path + "' to be watched.", iox);
			}
		}
		else
		{
			// A new file is created. We need to clear the NOT_FOUND entry that may have been added earlier.
			// MarkupCache keys are fully qualified URIs
			String absolutePath = path.toAbsolutePath().toFile().toURI().toString();

			try
			{
				ThreadContext.setApplication(application);
				application.getMarkupSettings()
						.getMarkupFactory().getMarkupCache().removeMarkup(absolutePath);
			} finally {
				ThreadContext.setApplication(null);
			}
		}
	}

	@Override
	public void destroy()
	{
		try
		{
			super.destroy();

			if (task != null)
			{
				task.interrupt();
			}
		} finally
		{
			IOUtils.closeQuietly(watchService);
		}
	}

	/**
	 * Registers all classpath folder entries and their subfolders in the {@code #watchService}.
	 * 
	 * @param watchService
	 *      the watch service that will send the notifications
	 * @throws IOException
	 */
	private void registerWatchables(final WatchService watchService) throws IOException
	{
		String classpath = System.getProperty("java.class.path");

		String[] classPathEntries = Strings.split(classpath, File.pathSeparatorChar);
		for (String classPathEntry : classPathEntries)
		{
			if (classPathEntry.endsWith(".jar") == false)
			{
				Path folder = Paths.get(classPathEntry);
				if (Files.isDirectory(folder))
				{
					register(folder, watchService);

					Files.walkFileTree(folder, new SimpleFileVisitor<Path>() {
						@Override
						public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
						{
							register(dir, watchService);
							return FileVisitResult.CONTINUE;
						}
					});
				}
			}
		}
	}
	
	private void register(final Path folder, final WatchService watchService) throws IOException
	{
		WatchEvent.Kind[] watchedKinds = getWatchedKinds(folder);
		LOG.debug("Registering folder '{}' to the watching service with kinds: {}", folder, watchedKinds);
		folder.register(watchService, watchedKinds);
	}

	/**
	 * @param folder
	 *          the folder that will be watched
	 * @return an array of watch event kinds to use for the watching of the given folder
	 */
	protected WatchEvent.Kind[] getWatchedKinds(Path folder)
	{
		return new WatchEvent.Kind[] {ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY};
	}
}
