package serverP2P;
import tools.Logger;
import tools.LogLevel;
import tools.HostItem;
import java.io.File;
import java.util.Vector;
import java.util.Arrays;
import java.util.Map;
import java.util.HashMap;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import protocolP2P.HashAlgorithm;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;

/** Class allowing to keep the tracker informed about file list
 * @author	Louis Royer
 * @author	Flavien Haas
 * @author	JS Auge
 * @version	1.0
 */
public abstract class FileWatcher implements Runnable {
	protected String[] fileList = new String[0];
	protected Logger logger;
	protected volatile boolean stop;
	protected long time;
	protected boolean force;
	protected HostItem server;
	protected HostItem tracker;
	protected String baseDirectory;
	protected Map<String, byte[]> sha512 = new HashMap<>();
	protected Thread thread;


	/** Constructor
	 * @param logger	Logger
	 * @param millis	Time interval before recheck
	 * @param server	HostItem for the server
	 * @param tracker	HostItem for the tracker
	 * @param baseDirectory	Directory to search files
	 */
	public FileWatcher(Logger logger, long millis, HostItem server, HostItem tracker, String baseDirectory) {
		assert logger != null : "Logger is null";
		assert server != null : "Server is null";
		assert tracker != null : "Tracker is null";
		assert baseDirectory != null : "baseDirectory is null";
		this.logger = logger;
		time = millis;
		this.server = server;
		this.tracker = tracker;
		this.baseDirectory = baseDirectory;
	}

	/** FileList getter
	 * @return fileList
	 */
	public String[] getFileList() {
		return fileList;
	}

	/** Sha512 map getter
	 * @return sha512 hashmap
	 */
	public Map<String, byte[]> getSha512Map() {
		return sha512;
	}

	/** Allow a manual check
	 */
	public void trigger() {
		if (updateFileList() || force) {
			force = false;
			writeLog("File list watcher detected changes. Informing tracker.", LogLevel.Info);
			registerTracker();
		}
	}

	/** Runnable implementation */
	public void run() {
		writeLog("File list watcher started : delay " + time + " milliseconds.", LogLevel.Info);
		while(!stop) {
			trigger();
			try {
				Thread.sleep(time);
			} catch(InterruptedException e) {
				writeLog("File list watcher interrupted", LogLevel.Info);
				setStop();
			}
		}
		writeLog("File watcher end of loop", LogLevel.Debug);
	}

	/** Register server on tracker
	 */
	protected abstract void registerTracker();

	/** Update fileList and returns true if different than old list.
	 * @return	true if changed
	 */
	protected boolean updateFileList() {
		File folder = new File(baseDirectory);
		Vector<String> v = new Vector<String>();
		File[] files = folder.listFiles();
		/* Add non-recursively files's names to fileList */
		for (File f : files) {
			if (f.isFile()) {
				v.add(f.getName());
			}
		}
		String[] newFileList = new String[v.size()];
		v.toArray(newFileList);
		Arrays.sort(newFileList);
		if (!Arrays.equals(newFileList, fileList)) {
			fileList = newFileList;
			initSha512();
			return true;
		} else {
			return false;
		}
	}

	
	/** Ask the thread to stop
	 */
	public void setStop() {
		stop = true;
		if (thread != null) {
			thread.interrupt();
		}
	}

	/** Init sha512 map.
	 */
	protected void initSha512() {
		for(String f: fileList) {
			try {
				MessageDigest md = MessageDigest.getInstance(HashAlgorithm.SHA512.getName());
				sha512.put(f, md.digest(Files.readAllBytes(Paths.get(baseDirectory + f))));
				md.reset();
			} catch (NoSuchAlgorithmException e) {
				writeLog("sha512 not supported", LogLevel.Error);
			} catch (IOException e) {
				writeLog("cannot read " + f, LogLevel.Warning);
			}
		}
	}

	/** Implementation of writeLog
	 * @param text	Text to log
	 * @param logLevel	level of logging
	 */
	protected abstract void writeLog(String text, LogLevel logLevel);

	/** Implementation of writeLog
	 * @param e	exception to log
	 * @param logLevel	level of logging
	 */
	protected abstract void writeLog(Exception e, LogLevel logLevel);

	/** Set thread
	 * @param thread Thread
	 */
	public void setThread(Thread thread) {
		this.thread = thread;
	} 

}
