/*-
 * ~~~~~~licensing~~~~~~
 * base-support
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.platform.base.support;

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.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.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.HashMap;
import java.util.Map;

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

public class SimpleFileWatcher implements Runnable {

    public static interface WatcherCallback {

        default void directoryCreated(WatchEvent<?> event, Path directory) {};

        default void directoryModified(WatchEvent<?> event, Path directory) {};

        default void directoryDeleted(WatchEvent<?> event, Path directory) {};

        default void fileCreated(WatchEvent<?> event, Path directory) {};

        default void fileModified(WatchEvent<?> event, Path directory) {};

        default void fileDeleted(WatchEvent<?> event, Path directory) {};

    }

    private static final Logger LOG = LoggerFactory.getLogger(SimpleFileWatcher.class);

    protected static final WatcherCallback NULL_CALLBACK = new WatcherCallback() {};

    protected final WatchService watchService;
    protected final Map<WatchKey, Path> keys;

    protected WatcherCallback watcherCallback = null;

    /**
     * Creates a WatchService and registers the given directory
     */
    public SimpleFileWatcher(Path dir) throws IOException {
        this.watchService = FileSystems.getDefault().newWatchService();
        this.keys = new HashMap<WatchKey, Path>();

        walkAndRegisterDirectories(dir);
    }

    /**
     * Register the given directory with the WatchService; This function will be called by
     * FileVisitor
     */
    private void registerDirectory(Path dir) throws IOException {
        WatchKey key = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        keys.put(key, dir);
    }

    /**
     * Register the given directory, and all its sub-directories, with the WatchService.
     */
    private void walkAndRegisterDirectories(final Path start) throws IOException {
        // register directory and sub-directories
        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                registerDirectory(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    public void setWatcherCallback(WatcherCallback watcherCallback) {
        this.watcherCallback = watcherCallback;
    }

    protected WatcherCallback getActiveCallback() {
        return watcherCallback == null ? NULL_CALLBACK : watcherCallback;
    }

    /**
     * Process all events for keys queued to the watchService
     */
    @Override
    public void run() {

        WatchKey key = null;
        try {
            while ((key = watchService.take()) != null) {

                Path dir = keys.get(key);
                if (dir == null) {
                    System.err.println("WatchKey not recognized!!");
                    continue;
                }

                for (WatchEvent<?> event : key.pollEvents()) {
                    @SuppressWarnings("rawtypes")
                    WatchEvent.Kind kind = event.kind();

                    // Context for directory entry event is the file name of entry
                    @SuppressWarnings("unchecked")
                    Path name = ((WatchEvent<Path>) event).context();
                    Path child = dir.resolve(name);

                    // print out event
                    LOG.debug("{}: {}\n", event.kind().name(), child);

                    // if directory is created, and watching recursively, then register it and its
                    // sub-directories

                    if (kind == ENTRY_CREATE) {
                        try {
                            if (Files.isDirectory(child)) {
                                walkAndRegisterDirectories(child);
                                getActiveCallback().directoryCreated(event, dir);
                            } else {
                                LOG.debug("FOUND FILE: {}", child.toString());
                                getActiveCallback().fileCreated(event, dir);
                            }
                        } catch (Exception x) {
                            LOG.error(String.format("FAILED processing event [%s] for [%s]", kind, child.toString()),
                                    x);
                        }
                    } else if (kind == ENTRY_MODIFY) {
                        try {
                            if (Files.isDirectory(child)) {
                                getActiveCallback().directoryModified(event, dir);
                            } else {
                                getActiveCallback().fileModified(event, dir);
                            }
                        } catch (Exception x) {
                            LOG.error(String.format("FAILED processing event [%s] for [%s]", kind, child.toString()),
                                    x);
                        }
                    } else if (kind == ENTRY_DELETE) {
                        try {
                            if (Files.isDirectory(child)) {
                                getActiveCallback().directoryDeleted(event, dir);
                            } else {
                                getActiveCallback().fileDeleted(event, dir);
                            }
                        } catch (Exception x) {
                            LOG.error(String.format("FAILED processing event [%s] for [%s]", kind, child.toString()),
                                    x);
                        }
                    }
                }

                // reset key and remove from set if directory no longer accessible
                boolean valid = key.reset();
                if (!valid) {
                    keys.remove(key);

                    // all directories are inaccessible
                    if (keys.isEmpty()) {
                        break;
                    }
                }
            }
        } catch (InterruptedException ignore) {
            // NOOP
        }
    }

    public void close() {
        try {
            for (WatchKey key : keys.keySet())
                key.cancel();
            this.watchService.close();
        } catch (IOException e) {
            LOG.error("FAILED closing SimpleFileWatcher", e);
        }
    }

}
