package ctbrec.recorder;

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

import java.io.File;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

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

import ctbrec.Config;
import ctbrec.Recording;

public class RecordingFileMonitor {

    private static final transient Logger LOG = LoggerFactory.getLogger(RecordingFileMonitor.class);
    private WatchService watcher;
    private Map<WatchKey, Path> keys;
    private boolean running = true;
    private RecordingManager manager;

    public RecordingFileMonitor(RecordingManager manager) throws IOException {
        this.manager = manager;
        this.watcher = FileSystems.getDefault().newWatchService();
        this.keys = new HashMap<>();
        registerAll(new File(Config.getInstance().getSettings().recordingsDir).toPath());
    }

    void processEvents() {
        while (running) {
            // wait for key to be signalled
            WatchKey key;
            try {
                key = watcher.take();
            } catch (InterruptedException | ClosedWatchServiceException x) {
                return;
            }

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

            List<WatchEvent<?>> events = key.pollEvents();
            LOG.debug("Size: {}", events.size());
            if (isRenameProcess(events)) {
                handleRename(dir, events);
            } else {
                for (WatchEvent<?> event : events) {
                    WatchEvent.Kind<?> kind = event.kind();

                    // TBD - provide example of how OVERFLOW event is handled
                    if (kind == OVERFLOW) {
                        continue;
                    }

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

                    if(Files.isRegularFile(child)) {
                        if (kind == ENTRY_CREATE) {
                            handleFileCreation(child);
                        } else if (kind == ENTRY_DELETE) {
                            handleFileDeletion(child);
                        }
                    } else {
                        if (kind == ENTRY_CREATE) {
                            handleDirCreation(child);
                        } else if (kind == ENTRY_DELETE) {
                            handleDirDeletion(child);
                        }
                    }
                }
            }

            // 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;
                }
            }
        }
    }

    private void handleRename(Path dir, List<WatchEvent<?>> events) {
        WatchEvent<Path> deleteEvent = cast(events.get(0));
        WatchEvent<Path> createEvent = cast(events.get(1));
        Path from = dir.resolve(deleteEvent.context());
        Path to = dir.resolve(createEvent.context());
        LOG.debug("{} -> {}", from, to);
        List<Recording> affectedRecordings = getAffectedRecordings(from);
        adjustPaths(affectedRecordings, from, to);
        if (Files.isDirectory(to, LinkOption.NOFOLLOW_LINKS)) {
            unregister(from);
            try {
                registerAll(to);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private List<Recording> getAffectedRecordings(Path from) {
        String f = from.toAbsolutePath().toString();
        List<Recording> affected = new ArrayList<>();
        for (Recording rec : manager.getAll()) {
            String r = rec.getAbsoluteFile().getAbsolutePath();
            if (r.startsWith(f)) {
                affected.add(rec);
            }
        }
        return affected;
    }

    private void adjustPaths(List<Recording> affectedRecordings, Path from, Path to) {
        for (Recording rec : affectedRecordings) {
            String oldPath = rec.getAbsoluteFile().getAbsolutePath();
            String newPath = oldPath.replace(from.toString(), to.toString());
            String recordingsDir = Config.getInstance().getSettings().recordingsDir;
            String relativePath = newPath.replaceFirst(Pattern.quote(recordingsDir), "");
            LOG.debug("Recording path has changed {} -> {}", rec.getPath(), relativePath);
            rec.setPath(relativePath);
            try {
                manager.saveRecording(rec);
            } catch (IOException e) {
                LOG.error("Couldn't update recording path in meta data file", e);
            }
        }
    }

    private void handleFileCreation(Path child) {
        LOG.trace("File created {}", child);
    }

    private void handleFileDeletion(Path child) {
        LOG.trace("File deleted {}", child);
    }

    private void handleDirCreation(Path dir) {
        try {
            registerAll(dir);
            LOG.trace("Directory added {}", dir);
        } catch (IOException x) {
            // ignore to keep sample readbale
        }
    }

    private void handleDirDeletion(Path dir) {
        // TODO unregister key ?!?

        // only delete directories, which have actually been deleted
        if(Files.notExists(dir, LinkOption.NOFOLLOW_LINKS)) {
            LOG.trace("Directory Deleted {}", dir);
        }
    }

    private boolean isRenameProcess(List<WatchEvent<?>> events) {
        if(events.size() == 2) {
            boolean deleteFirst = events.get(0).kind() == ENTRY_DELETE;
            boolean createSecond = events.get(1).kind() == ENTRY_CREATE;
            return deleteFirst && createSecond;
        } else {
            return false;
        }
    }

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

    /**
     * Register the given directory with the WatchService
     */
    void register(Path dir) {
        try {
            WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE);
            keys.put(key, dir);
            LOG.debug("Monitor {}", dir);
        } catch(IOException e) {
            LOG.warn("Couldn't register directory monitor for directory {}", dir, e);
        }
    }

    public void unregister(Path path) {

    }

    @SuppressWarnings("unchecked")
    static <T> WatchEvent<T> cast(WatchEvent<?> event) {
        return (WatchEvent<T>) event;
    }

    public void addDirectory(Path dir) throws IOException {
        LOG.info("Adding monitor for {}", dir);
        registerAll(dir);
    }

    public void stop() throws IOException {
        running = false;
        watcher.close();
    }
}
