/**
 * The MIT License
 * Copyright © 2022 Luis Andrés Lange <http://javacomm.net>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.nexuswob.util;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;



/**
 * Der WatchService beobachtet reguläre Dateien in einem Verzeichnis. Versteckte
 * Dateien, symbolische Links und Verzeichnisse in Verzeichnisse sind keine
 * regulären Dateien. Wenn eine Datei in das Verezeichnis kopiert oder entfernt
 * wird, wird ein {@code WatchEvent} ausgelöst.
 *
 *
 *
 * @author llange
 *
 */
public class WatchService {

  final Logger log = LogManager.getLogger(WatchService.class);
  private List<WatchListener> watchlistenerList = Collections
      .synchronizedList(new LinkedList<WatchListener>());

  private Path uploadDir;
  private List<Path> alteliste;

  public WatchService() {
    alteliste = new ArrayList<>();
    uploadDir = Paths.get("");
  }



  /**
   * Ein Beobachter meldet sich an.
   *
   * @param listener
   *                 ein Beobachter
   */
  public synchronized void addWatchListener(WatchListener listener) {
    watchlistenerList.add(listener);
  }



  /**
   * Vergleiche zwei Verzeichnislisten. Sind die Listen gleich, wird {@code true}
   * zurückgeliefert.
   *
   *
   * @param alteliste
   *                  eine Dateiliste zu einem Zeitpunkt t0
   * @param neueliste
   *                  eine Dateiliste zu einem Zeitpunkt t0 + n-Sekunden
   *
   * @return {@code true}, beide Listen sind gleich
   */
  boolean compare(List<Path> alteliste, List<Path> neueliste) {
    if (alteliste.size() != neueliste.size()) return false;
    for (Path alt : alteliste) {
      boolean found = false;
      for (Path neu : neueliste) {
        found = found || alt.equals(neu);
      }
      if (!found) return false;
    }
    return true;
  }



  /**
   * Alle Beobachter werden abgemeldet.
   *
   */
  public synchronized void removAllListener() {
    watchlistenerList.clear();
  }



  /**
   * Ein Beoabcher meldet sich ab.
   *
   * @param listener
   *                 ein Beobachter
   */
  public synchronized void removeWatchListener(WatchListener listener) {
    watchlistenerList.remove(listener);
  }



  /**
   * Das zu überwachende Verzeichnis wird gesetzt. Nach einem Verzeichniswechsel
   * wird immer ein {@code WatchEvent} ausgelöst, wenn mindestens 1 reguläre Datei
   * im neuen Verzeichnis liegt.
   *
   * @param path
   *             ein Verzeichnis
   * @throws NoDirectoryException
   *                              Parameter {@code path} ist kein Verzeichnis
   */
  public synchronized void setDirectory(Path dir) throws NoDirectoryException {
    if (dir == null) throw new IllegalArgumentException("dir is null");
    if (!Files.isDirectory(dir)) throw new NoDirectoryException(dir + " - ist kein Verzeichnis");
    alteliste.clear();
    uploadDir = dir;
  }



  /**
   * Die Task wird von einem ScheduledExecutor ausgeführt. Der
   * Anwendungsentwickler definiert das Zeitintervall für den ScheduledExecutor.
   * Events werden erst durchgestellt, wenn die Task ausgeführt wird.
   *
   * @return eine Task
   */
  public Runnable task() {

    Runnable task = () -> {
      try {
        alteliste = watchDir(uploadDir);
      }
      catch (NoDirectoryException | IOException e) {
        log.info(e.getMessage());
      }
    };

    return task;
  }



  /**
   * Im Uploadverzeichnis werden nur reguläre Dateien beobachtet.
   *
   * @param dir
   *            dieses Verzeichnis wird überwacht
   *
   * @return alle regulären Dateien im Uploadverzeichnis
   *
   * @throws NoDirectoryException
   *                              {@code dir} ist kein Verzeichnis
   * @throws IOException
   *                              das Uploadverzeichnis kann nicht gelesen werden
   */
  private List<Path> watchDir(Path dir) throws NoDirectoryException, IOException {

    ArrayList<Path> neueliste = new ArrayList<>();
    try(DirectoryStream<Path> directory = Files.newDirectoryStream(dir)) {
      directory.forEach((path) -> {
        if (Files.isRegularFile(path)) {
          neueliste.add(path);
        }
      });
    }

    if (!compare(alteliste, neueliste)) {
      // event auslösen
      WatchEvent event = new WatchEvent(this, neueliste);
      watchlistenerList.forEach((listener) -> {
        listener.onChanged(event);
      });
    }
    return neueliste;
  }

}
