package com.airlock.iam.base.misc.io;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchEvent.Modifier;
import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public final class FileWatcher implements AutoCloseable {
   private static final ELogger LOG = ELoggerFactory.getLogger(FileWatcher.class);
   private final ExecutorService executorService = Executors.newSingleThreadExecutor();
   private final WatchService watchService;
   private final FileWatcher.CreationInfo creationInfo;

   @SuppressFBWarnings(
      value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE",
      justification = "requireNonNull is not detected: https://github.com/spotbugs/spotbugs/issues/651"
   )
   public FileWatcher(String creator, Collection<Path> filesToWatch, Runnable eventListener) {
      this.creationInfo = new FileWatcher.CreationInfo(creator);

      try {
         this.watchService = FileSystems.getDefault().newWatchService();

         for (Path f : filesToWatch) {
            Objects.requireNonNull(f.getParent()).register(this.watchService, allEvents(), modifiers());
         }

         this.executorService.execute(() -> {
            WatchKey key;
            do {
               try {
                  key = this.watchService.take();
               } catch (InterruptedException e) {
                  Thread.currentThread().interrupt();
                  return;
               }

               for (WatchEvent<?> event : key.pollEvents()) {
                  if (event.kind() != StandardWatchEventKinds.OVERFLOW) {
                     Path dir = (Path)key.watchable();
                     Path file = dir.resolve((Path)event.context());
                     if (filesToWatch.contains(file)) {
                        LOG.debug(this.creationInfo + " " + event.kind() + ": " + file);
                        eventListener.run();
                     }
                  }
               }
            } while (key.reset());
         });
      } catch (IOException e) {
         throw new FailureException(e);
      }
   }

   private static Kind<?>[] allEvents() {
      return new Kind[]{StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY};
   }

   private static Modifier[] modifiers() {
      try {
         return new Modifier[]{sensititityModifier()};
      } catch (ReflectiveOperationException e) {
         return new Modifier[0];
      }
   }

   private static Modifier sensititityModifier() throws ReflectiveOperationException {
      Class<?> sensitivityModifierType = Class.forName("com.sun.nio.file.SensitivityWatchEventModifier");
      return (Modifier)sensitivityModifierType.getField("HIGH").get(null);
   }

   @Override
   public void close() {
      try {
         this.executorService.shutdownNow();
         this.executorService.awaitTermination(1L, TimeUnit.SECONDS);
         this.watchService.close();
         LOG.debug(this.creationInfo + " closed.");
      } catch (InterruptedException e) {
         Thread.currentThread().interrupt();
      } catch (IOException e) {
         throw new FailureException(e);
      }
   }

   private static final class CreationInfo {
      private final String creator;
      private final long createdAt;

      CreationInfo(String creator) {
         this.creator = creator;
         this.createdAt = System.currentTimeMillis();
      }

      @Override
      public String toString() {
         return "Created{from='" + this.creator + "', " + (System.currentTimeMillis() - this.createdAt) + "ms ago}";
      }
   }
}
