package ctbrec;

import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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

@Slf4j
public class RecordingSizeMonitor {

    protected final WatchService service;
    protected final Map<WatchKey, Path> keys;
    protected final Map<WatchKey, Recording> recordingByKey;
    protected final Map<Recording, List<WatchKey>> keysByRecording;
    protected final Set<Path> registeredPaths;
    private final Lock lock = new ReentrantLock();

    public RecordingSizeMonitor() throws IOException {
        this.service = FileSystems.getDefault().newWatchService();
        this.keys = new HashMap<>();
        this.recordingByKey = new HashMap<>();
        this.keysByRecording = new HashMap<>();
        this.registeredPaths = new HashSet<>();
    }

    public void monitor(Recording rec) {
        GlobalThreadPool.submit(() -> {
            try {
                for (File file : rec.getAllRecordingFiles()) {
                    for (int i = 0; i < 300; i++) { // wait up to 30 seconds for a file to show up
                        Path path = file.toPath();
                        if (Files.exists(path)) {
                            if (Files.isDirectory(path, NOFOLLOW_LINKS)) {
                                registerAll(path, rec);
                            } else if (Files.isRegularFile(path)) {
                                registerAll(path.getParent(), rec);
                            }
                            break;
                        } else {
                            Thread.sleep(100);
                        }
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } catch (Exception e) {
                log.warn("Couldn't register file monitor for {}", rec, e);
            }
        });
    }

    private synchronized void register(Path path, Recording rec) throws IOException {
        lock.lock();
        try {
            if (!registeredPaths.contains(path)) {
                WatchKey key = path.register(service, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
                keys.put(key, path);
                recordingByKey.put(key, rec);
                keysByRecording.computeIfAbsent(rec, r -> new ArrayList<>()).add(key);
                registeredPaths.add(path);
            }
        } finally {
            lock.unlock();
        }
    }

    private void registerAll(Path path, Recording rec) throws IOException {
        Files.walkFileTree(path, new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                register(dir, rec);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    public void uninstall(Recording rec) {
        lock.lock();
        try {
            List<WatchKey> keysForRecording = this.keysByRecording.getOrDefault(rec, Collections.emptyList());
            keysForRecording.forEach(key -> {
                Path path = keys.get(key);
                key.cancel();
                keys.remove(key);
                recordingByKey.remove(key);
                registeredPaths.remove(path);
            });
            this.keysByRecording.remove(rec);
        } finally {
            lock.unlock();
        }
    }

    public void processEvents() {
        while (!Thread.interrupted()) {
            WatchKey key = null;
            try {
                try {
                    key = service.take();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    continue;
                }

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

                for (WatchEvent<?> event : key.pollEvents()) {
                    WatchEvent.Kind<?> kind = event.kind();
                    Recording r = recordingByKey.get(key);
                    if (r == null) {
                        continue;
                    }

                    // events might have been lost, we just refresh the recording to be sure
                    if (kind == OVERFLOW) {
                        r.refresh();
                        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);

                    try {
                        r.refresh(dir, ev);
                    } catch (IOException e) {
                        log.error("Error while updating recording size of {}", r, e);
                    }

                    // if directory is created, and watching recursively, then
                    // register it and its sub-directories
                    if (kind == ENTRY_CREATE && Files.isDirectory(child, NOFOLLOW_LINKS)) {
                        registerAll(child, r);
                    }
                }
            } catch (Exception e) {
                log.error("Error while processing file system events", e);
            } finally {
                lock.unlock();
                if (key != null) {
                    key.reset();
                }
            }
        }
    }

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