package net.morimekta.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static com.sun.nio.file.SensitivityWatchEventModifier.HIGH;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

/**
 * File watcher helper for use with simple callbacks.
 */
public class FileWatcher implements AutoCloseable {
    @FunctionalInterface
    public interface Watcher {
        void onFileUpdate(File file);
    }

    public FileWatcher() {
        this(newWatchService(),
             Executors.newSingleThreadExecutor(),
             Executors.newSingleThreadExecutor());
    }

    // @VisibleForTesting
    protected FileWatcher(WatchService watchService,
                          ExecutorService watcherExecutor,
                          ExecutorService callbackExecutor) {
        this.watchers = new LinkedList<>();
        this.watchDirKeys = new HashMap<>();
        this.watchKeyDirs = new HashMap<>();
        this.watchedFiles = Collections.synchronizedSet(new HashSet<>());
        this.watchService = watchService;
        this.watcherExecutor = watcherExecutor;
        this.callbackExecutor = callbackExecutor;
        this.watcherExecutor.submit(this::watchFilesTask);
    }

    public void addWatcher(Watcher watcher) {
        if (watcher == null) {
            throw new IllegalArgumentException("Null watcher added");
        }

        synchronized (watchers) {
            if (watcherExecutor.isShutdown()) {
                throw new IllegalStateException("Adding watcher on closed FileWatcher");
            }
            watchers.add(watcher);
        }
    }

    public boolean removeWatcher(Watcher watcher) {
        if (watcher == null) {
            throw new IllegalArgumentException("Null watcher removed");
        }

        synchronized (watchers) {
            return watchers.remove(watcher);
        }
    }

    public void startWatching(File file) {
        try {
            if (file == null) {
                throw new IllegalArgumentException("Null file argument");
            }
            if (watcherExecutor.isShutdown()) {
                throw new IllegalStateException("Starts to watch on closed FileWatcher");
            }

            file = file.getCanonicalFile()
                       .getAbsoluteFile();

            if (watchedFiles.contains(file.toString())) {
                // We're already watching this file, do nothing.
                return;
            }

            File parent = file.getParentFile();
            if (!watchDirKeys.containsKey(parent.toString())) {

                Path dirPath = Paths.get(parent.getAbsolutePath());
                WatchKey key = dirPath.register(watchService, new WatchEvent.Kind[]{ENTRY_MODIFY, ENTRY_CREATE}, HIGH);
                watchDirKeys.put(parent.toString(), key);
                watchKeyDirs.put(key, parent.toString());
            }

            watchedFiles.add(file.toString());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public void stopWatching(File file) {
        try {
            if (file == null) {
                throw new IllegalArgumentException("Null file argument");
            }
            file = file.getCanonicalFile()
                       .getAbsoluteFile();

            watchedFiles.remove(file.toString());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public void close() throws IOException {
        synchronized (watchers) {
            watchers.clear();
        }
        watcherExecutor.shutdown();
        watchService.close();
    }

    /**
     * Handle the watch service event loop.
     */
    private void watchFilesTask() {
        while (!watcherExecutor.isShutdown()) {
            try {
                WatchKey key = watchService.take();
                String parent = watchKeyDirs.get(key);
                if (parent == null) {
                    key.reset();
                    continue;
                }

                Set<File> updates = new TreeSet<>();
                for (WatchEvent<?> ev : key.pollEvents()) {
                    WatchEvent.Kind<?> kind = ev.kind();

                    // Only file modification is interesting.
                    if (kind != ENTRY_MODIFY && kind != ENTRY_CREATE) {
                        if (kind == OVERFLOW) {
                            LOGGER.warn("Overflow event, file updates may have been lost");
                        }
                        continue;
                    }

                    @SuppressWarnings("unchecked")
                    WatchEvent<Path> event = (WatchEvent<Path>) ev;

                    File file = new File(parent, event.context().toString());
                    if (watchedFiles.contains(file.toString())) {
                        LOGGER.trace("Watched file " + file + " event " + kind);
                        updates.add(file);
                    }
                }
                // Ready the key again so it vil signal more events.
                key.reset();

                if (updates.size() > 0) {
                    List<Watcher> tmp = new LinkedList<>();
                    synchronized (watchers) {
                        tmp.addAll(watchers);
                    }
                    callbackExecutor.submit(() -> {
                        for (final Watcher watcher : tmp) {
                            for (final File file : updates) {
                                watcher.onFileUpdate(file);
                            }
                        }
                    });
                }
            } catch (InterruptedException interruptedEx) {
                LOGGER.error("Interrupted in service file watch thread: " + interruptedEx.getMessage(), interruptedEx);
                return;
            }
        }
    }

    private static WatchService newWatchService() {
        try {
            return FileSystems.getDefault().newWatchService();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private static final Logger LOGGER = LoggerFactory.getLogger(FileWatcher.class);

    private final LinkedList<Watcher>   watchers;
    private final Map<String, WatchKey> watchDirKeys;
    private final Map<WatchKey, String> watchKeyDirs;
    private final Set<String>           watchedFiles;
    private final ExecutorService       callbackExecutor;
    private final ExecutorService       watcherExecutor;
    private final WatchService          watchService;
}

@@ -641,16 +641,109 @@
 ecutors;
+%0Aimport java.util.concurrent.ThreadFactory;%0Aimport java.util.concurrent.atomic.AtomicInteger;
 %0A%0Aimport
@@ -1319,16 +1319,48 @@
 xecutor(
+makeThreadFactory(%22FileWatcher%22)
 ),%0A     
@@ -1401,16 +1401,56 @@
 xecutor(
+makeThreadFactory(%22FileWatcherCallback%22)
 ));%0A    
@@ -4492,24 +4492,395 @@
 e();%0A    %7D%0A%0A
+    private static ThreadFactory makeThreadFactory(String name) %7B%0A        AtomicInteger idx = new AtomicInteger();%0A        return runnable -%3E %7B%0A            Thread thread = Executors.defaultThreadFactory().newThread(runnable);%0A            thread.setName(name + %22-%22 + idx.incrementAndGet());%0A            thread.setDaemon(true);%0A            return thread;%0A        %7D;%0A    %7D%0A%0A
     /**%0A    
@@ -6697,24 +6697,66 @@
  updates) %7B%0A
+                                try %7B%0A    
             
@@ -6795,32 +6795,228 @@
 leUpdate(file);%0A
+                                %7D catch (RuntimeException e) %7B%0A                                    LOGGER.error(%22Exception when notifying update on %22 + file, e);%0A                                %7D%0A
                 

Make FileWatcher threads into daemon threads.