/*
 * Copyright (c) 2016 Farooq Khan
 *
 * 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 io.jsondb.events;

import java.io.File;
import java.io.IOException;
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.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import io.jsondb.CollectionMetaData;
import io.jsondb.JsonDBConfig;
import io.jsondb.JsonDBException;

/**
 * A class that holds a list of CollectionFileChangeListeners.
 * @version 1.0 15-Oct-2016
 */
public class EventListenerList {
  private Logger logger = LoggerFactory.getLogger(EventListenerList.class);

  private JsonDBConfig dbConfig = null;
  private Map<String, CollectionMetaData> cmdMap;

  private List<CollectionFileChangeListener> listeners;
  private ExecutorService collectionFilesWatcherExecutor;
  private WatchService watcher = null;
  private boolean stopWatcher;

  public EventListenerList(JsonDBConfig dbConfig, Map<String, CollectionMetaData> cmdMap) {
    this.dbConfig = dbConfig;
    this.cmdMap = cmdMap;
  }

  public void addCollectionFileChangeListener(CollectionFileChangeListener listener) {
    if (null == listeners) {
      listeners = new ArrayList<CollectionFileChangeListener>();

      listeners.add(listener);

      collectionFilesWatcherExecutor = Executors.newSingleThreadExecutor(
          new ThreadFactoryBuilder().setNameFormat("jsondb-files-watcher-thread-%d").build());

      try {
        watcher = dbConfig.getDbFilesPath().getFileSystem().newWatchService();
        dbConfig.getDbFilesPath().register(watcher, StandardWatchEventKinds.ENTRY_CREATE,
                                                    StandardWatchEventKinds.ENTRY_DELETE,
                                                    StandardWatchEventKinds.ENTRY_MODIFY);
      } catch (IOException e) {
        logger.error("Failed to create the WatchService for the dbFiles location", e);
        throw new JsonDBException("Failed to create the WatchService for the dbFiles location", e);
      }

      collectionFilesWatcherExecutor.execute(new CollectionFilesWatcherRunnable());
    } else {
      listeners.add(listener);
    }
  }

  public void removeCollectionFileChangeListener(CollectionFileChangeListener listener) {
    if (null != listeners) {
      listeners.remove(listener);
    }
    if (listeners.size() < 1) {
      stopWatcher = true;
      collectionFilesWatcherExecutor.shutdownNow();
      try {
        watcher.close();
      } catch (IOException e) {
        logger.error("Failed to close the WatchService for the dbFiles location", e);
      }
    }
  }

  public boolean hasCollectionFileChangeListener() {
    if ((null != listeners) && (listeners.size() > 0)) {
      return true;
    }
    return false;
  }

  public void shutdown() {
    if (null != listeners && listeners.size() > 0) {
      stopWatcher = true;
      collectionFilesWatcherExecutor.shutdownNow();
      try {
        watcher.close();
      } catch (IOException e) {
        logger.error("Failed to close the WatchService for the dbFiles location", e);
      }
      listeners.clear();
    }
  }

  private class CollectionFilesWatcherRunnable implements Runnable {
    @Override
    public void run() {
      while (!stopWatcher) {
        WatchKey watckKey = null;
        try {
          watckKey = watcher.take();
        } catch (InterruptedException e) {
          logger.debug("The watcher service thread was interrupted", e);
          return;
        }
        List<WatchEvent<?>> events = watckKey.pollEvents();
        for (WatchEvent<?> event : events) {
          WatchEvent.Kind<?> kind = event.kind();
          if (kind == StandardWatchEventKinds.OVERFLOW) {
            continue;
          }
          @SuppressWarnings("unchecked")
          WatchEvent<Path> ev = (WatchEvent<Path>)event;
          File file = ev.context().toFile();
          String fileName = file.getName();
          int extnLocation = fileName.lastIndexOf('.');
          if(extnLocation != -1) {
            String collectionName = fileName.substring(0, extnLocation);
            if (fileName.endsWith(".json") && (cmdMap.containsKey(collectionName))) {
              if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
                for (CollectionFileChangeListener listener : listeners) {
                  listener.collectionFileAdded(collectionName);
                }
              } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
                for (CollectionFileChangeListener listener : listeners) {
                  listener.collectionFileDeleted(collectionName);
                }
              } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
                for (CollectionFileChangeListener listener : listeners) {
                  listener.collectionFileModified(collectionName);
                }
              }
            }
          }
        }
      }
    }
  }
}