package sangria.gateway.file

import akka.actor._
import better.files.{Disposable, File, newMultiMap, repeat}
import better.files._

import scala.collection.mutable

/**
  * An actor that can watch a file or a directory
  * Instead of directly calling the constructor of this, call file.newWatcher to create the actor
  *
  * @param file     watch this file (or directory)
  * @param maxDepth In case of directories, how much depth should we watch
  */
class FileWatcher(file: File, maxDepth: Int) extends Actor {
  import FileWatcher._

  def this(file: File, recursive: Boolean = true) = this(file, if (recursive) Int.MaxValue else 0)

  protected[this] val callbacks = newMultiMap[Event, Callback]

  protected[this] val monitor: File.Monitor = new FileMonitor(file, maxDepth) {
    override def onEvent(event: Event, file: File, count: Int) = self ! Message.NewEvent(event, file, count)
    override def onException(exception: Throwable) = self ! Status.Failure(exception)
  }

  override def preStart() = monitor.start()(executionContext = context.dispatcher)

  override def receive = {
    case Message.NewEvent(event, target, count) if callbacks.contains(event) => callbacks(event).foreach(f => repeat(count)(f(event -> target)))
    case Message.RegisterCallback(events, callback) => events.foreach(event => callbacks.addBinding(event, callback))
    case Message.RemoveCallback(event, callback) => callbacks.removeBinding(event, callback)
  }

  override def postStop() = monitor.stop()
}

object FileWatcher {
  import java.nio.file.{Path, WatchEvent}

  type Event = WatchEvent.Kind[Path]
  type Callback = PartialFunction[(Event, File), Unit]

  sealed trait Message
  object Message {
    case class NewEvent(event: Event, file: File, count: Int) extends Message
    case class RegisterCallback(events: Traversable[Event], callback: Callback) extends Message
    case class RemoveCallback(event: Event, callback: Callback) extends Message
  }

  implicit val disposeActorSystem: Disposable[ActorSystem] =
    Disposable(_.terminate())

  implicit class FileWatcherOps(file: File) {
    def watcherProps(recursive: Boolean): Props =
      Props(new FileWatcher(file, recursive))

    def newWatcher(recursive: Boolean = true)(implicit ctx: ActorRefFactory): ActorRef =
      ctx.actorOf(watcherProps(recursive))
  }

  def when(events: Event*)(callback: Callback): Message =
    Message.RegisterCallback(events, callback)

  def on(event: Event)(callback: File => Unit): Message =
    when(event) { case (`event`, file) => callback(file) }

  def stop(event: Event, callback: Callback): Message =
    Message.RemoveCallback(event, callback)

  private def newMultiMap[A, B]: mutable.MultiMap[A, B] = new mutable.HashMap[A, mutable.Set[B]] with mutable.MultiMap[A, B]
  @inline private def repeat[U](n: Int)(f: => U): Unit = (1 to n).foreach(_ ⇒ f)
}