package com.msopentech.thali.java.toronionproxy;

import com.sun.nio.file.SensitivityWatchEventModifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.concurrent.TimeUnit;

public class JavaWatchObserver implements WriteObserver {
    private WatchService watchService;
    private WatchKey key;
    private File fileToWatch;
    private long lastModified;
    private long length;
    private static final Logger LOG = LoggerFactory.getLogger(WriteObserver.class);


    public JavaWatchObserver(File fileToWatch) throws IOException {
        if (fileToWatch == null || !fileToWatch.exists()) {
            throw new RuntimeException("fileToWatch must not be null and must already exist.");
        }
        this.fileToWatch = fileToWatch;
        lastModified = fileToWatch.lastModified();
        length = fileToWatch.length();

        watchService = FileSystems.getDefault().newWatchService();
        // Note that poll depends on us only registering events that are of type path
        if (OsData.getOsType() != OsData.OsType.MAC) {
            key = fileToWatch.getParentFile().toPath().register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE,
                    StandardWatchEventKinds.ENTRY_MODIFY);
        } else {
            // Unfortunately the default watch service on Mac is broken, it uses a separate thread and really slow polling to detect file changes
            // rather than integrating with the OS. There is a hack to make it poll faster which we can use for now. See
            // http://stackoverflow.com/questions/9588737/is-java-7-watchservice-slow-for-anyone-else
            key = fileToWatch.getParentFile().toPath().register(watchService, new WatchEvent.Kind[]
                    {StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE,
                    StandardWatchEventKinds.ENTRY_MODIFY}, SensitivityWatchEventModifier.HIGH);
        }
    }

    @Override
    public boolean poll(long timeout, TimeUnit unit) {
        boolean result = false;
        try {
            long remainingTimeoutInNanos = unit.toNanos(timeout);
            while (remainingTimeoutInNanos > 0) {
                long startTimeInNanos = System.nanoTime();
                WatchKey receivedKey = watchService.poll(remainingTimeoutInNanos, TimeUnit.NANOSECONDS);
                long timeWaitedInNanos = System.nanoTime() - startTimeInNanos;

                if (receivedKey != null) {
                    if (receivedKey != key) {
                        throw new RuntimeException("This really shouldn't have happened. EEK!" + receivedKey.toString());
                    }

                    for (WatchEvent<?> event : receivedKey.pollEvents()) {
                        WatchEvent.Kind<?> kind = event.kind();

                        if (kind == StandardWatchEventKinds.OVERFLOW ) {
                            LOG.error("We got an overflow, there shouldn't have been enough activity to make that happen.");
                        }


                        Path changedEntry = (Path) event.context();
                        if (fileToWatch.toPath().endsWith(changedEntry)) {
                            result = true;
                            return result;
                        }
                    }

                    // In case we haven't yet gotten the event we are looking for we have to reset in order to
                    // receive any further notifications.
                    if (!key.reset()) {
                        LOG.error("The key became invalid which should not have happened.");
                    }
                }

                if (timeWaitedInNanos >= remainingTimeoutInNanos) {
                    break;
                }

                remainingTimeoutInNanos -= timeWaitedInNanos;
            }

            // Even with the high sensitivity setting above for the Mac the polling still misses changes so I've added
            // a last modified check as a backup. Except I personally witnessed last modified not returning a new value
            // value even when I saw the file change!!!! So I'm also adding in a length check. Java really seems to
            // have an issue with the OS/X file system.
            result = (fileToWatch.lastModified() != lastModified) || (fileToWatch.length() != length);
            return result;
        } catch (InterruptedException e) {
            throw new RuntimeException("Internal error has caused JavaWatchObserver to not be reliable.", e);
        } finally {
            if (result) {
                try {
                    watchService.close();
                } catch (IOException e) {
                    LOG.debug("Attempt to close watchService failed.", e);
                }
            }
        }
    }
}