package charactermanaj.model.io;

import java.io.File;
import java.io.FileFilter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.concurrent.Semaphore;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.CRC32;

import charactermanaj.model.AppConfig;
import charactermanaj.model.CharacterData;
import charactermanaj.model.Layer;
import charactermanaj.model.PartsCategory;
import charactermanaj.util.ApplicationLogger;

/**
 * パーツファイルのディレクトリの監視を行うスレッド.<br>
 * 厳密に変更を監視しているわけではなく、アイテム数が変化するか、アイテムの最大の最終更新日が変わるか、
 * ファイル名と更新日をもとにしたハッシュ値が変化した場合に変化があったとみなす.<br>
 * ごく希に変化を検知しないかもしれないが、厳密な精度は要求していないので。
 * @author seraphy
 */
public class PartsImageDirectoryWatchAgent implements Runnable {

	/**
	 * ロガー
	 */
	private static final Logger logger = ApplicationLogger.getLogger();
	
	private final CharacterData characterData;
	
	private final File baseDir;
	
	/**
	 * 停止フラグ
	 */
	private volatile boolean stopFlag;
	
	/**
	 * 監視インターバル
	 */
	private int dirWatchInterval;
	
	/**
	 * スレッド、生成されていなければnull
	 */
	private Thread thread;
	
	/**
	 * 手動およびスレッドからの自動の二つの監視が同時に行われないようにするためのセマフォ
	 */
	private final Semaphore semaphore = new Semaphore(1);

	
	/**
	 * 監視結果1、まだ監視されていなければnull
	 */
	private volatile Long signature;
	
	/**
	 * 監視結果2、検出されたアイテムの個数
	 */
	private volatile int itemCount;
	
	/**
	 * 監視結果3、検出されたアイテムの最終更新日。ただし未来の日付は除外する。
	 */
	private volatile long maxLastModified;

	
	/**
	 * 監視を通知されるリスナー
	 */
	private LinkedList<PartsImageDirectoryWatchListener> listeners
			= new LinkedList<PartsImageDirectoryWatchListener>();

	
	public PartsImageDirectoryWatchAgent(CharacterData characterData) {
		if (characterData == null) {
			throw new IllegalArgumentException();
		}

		URL docBase = characterData.getDocBase();
		File baseDir = null;
		if (docBase != null && docBase.getProtocol().equals("file")) {
			baseDir = new File(docBase.getPath()).getParentFile();
		}

		this.characterData = characterData;
		this.baseDir = baseDir;
		this.dirWatchInterval = AppConfig.getInstance().getDirWatchInterval();
	}
	
	public CharacterData getCharcterData() {
		return characterData;
	}
	
	/**
	 * 監視を開始する.<br>
	 * 一定時間待機後、フォルダを監視し、前回監視結果と比較して変更があれば通知を行う.<br>
	 * その処理をstop指示が来るまで繰り返す.<br>
	 * 開始しても、まず指定時間待機するため、すぐにはディレクトリの走査は行わない.<br>
	 * 且つ、初回監視結果は変更通知されないため、二回以上走査しないかぎり通知は発生しない.<br>
	 * これは意図したものである.<br>
	 * 明示的に初回監視を行うには明示的に{@link #reset()}を呼び出す.<br>
	 */
	public void start() {
		synchronized (this) {
			if (thread != null) {
				throw new IllegalStateException();
			}
			stopFlag = false;
			thread = new Thread(this);
			thread.setDaemon(true);
			thread.start();
		}
	}
	
	/**
	 * 監視を停止する.<br>
	 * @return 停止した場合はtrue、すでに停止していたか開始されていない場合はfalse
	 */
	public boolean stop() {
		boolean stopped = false;
		synchronized (this) {
			if (thread != null && thread.isAlive()) {
				stopFlag = true;
				thread.interrupt();
				try {
					thread.join(10000); // 10Secs
				} catch (InterruptedException ex) {
					logger.log(Level.INFO, "stop request interrupted.", ex);
					// 抜ける
				}
				stopped = true;
			}
			thread = null;
		}
		return stopped;
	}
	
	
	/**
	 * スレッドの停止フラグがたてられるまで、一定時間待機と監視とを永久に繰り返す.<br>
	 * ただし、スレッド自身はデーモンとして動作させているので他の非デーモンスレッドが存在しなくなれば停止する.<br>
	 */
	public void run() {
		logger.log(Level.FINE, "watch-dir thead started.");
		while (!stopFlag) {
			try {
				Thread.sleep(dirWatchInterval);
				semaphore.acquire();
				try {
					watch();
				} finally {
					semaphore.release();
				}
			} catch (InterruptedException ex) {
				logger.log(Level.FINE, "watch-dir thead interrupted.", ex);
				// 何もしない
			} catch (Exception ex) {
				logger.log(Level.SEVERE, "PartsImageDirectoryWatchAgent failed.", ex);
				// 何もしない.
			}
		}
		logger.log(Level.INFO, "watch-dir thead stopped.");
	}
	
	/**
	 * 監視を行う.<br>
	 * 停止フラグが設定されるか、割り込みされた場合は処理を中断してInterruptedException例外を返して終了する.<br>
	 * @throws InterruptedException 割り込みされた場合
	 */
	public void watch() throws InterruptedException {
		if (baseDir == null || !baseDir.exists() || !baseDir.isDirectory()) {
			return;
		}

		int itemCount = 0;
		long maxLastModified = 0;
		long now = System.currentTimeMillis() + dirWatchInterval;
		
		CRC32 crc = new CRC32();
		for (PartsCategory partsCategory : characterData.getPartsCategories()) {
			for (Layer layer : partsCategory.getLayers()) {
				String dir = layer.getDir();
				File watchDir = new File(baseDir, dir);
				ArrayList<String> files = new ArrayList<String>();
				if (watchDir.exists() && watchDir.isDirectory()) {
					for (File file : watchDir.listFiles(new FileFilter() {
						public boolean accept(File pathname) {
							return pathname.isFile() && pathname.getName().toLowerCase().endsWith(".png");
						}
					})) {
						if (Thread.interrupted() || stopFlag) {
							throw new InterruptedException();
						}

						itemCount++;
						long lastModified = file.lastModified();
						if (lastModified <= now) {
							// 未来の日付は除外する.
							// 未来の日付のファイルが一つでもあると他のファイルが実際に更新されても判定できなくなるため。
							maxLastModified = Math.max(maxLastModified, lastModified);
						}
						files.add(file.getName() + ":" + lastModified);
					};
					Collections.sort(files);
				}
				for (String file : files) {
					crc.update(file.getBytes());
				}
			}
		}

		long signature = crc.getValue();
		if (this.signature != null) {
			// 初回は無視される.
			if (this.signature.longValue() != signature
					|| this.itemCount != itemCount
					|| this.maxLastModified != maxLastModified) {
				// ハッシュ値が異なるか、アイテム数が異なるか、最大の最終更新日が異なる場合、変更されたとみなす.
				fireWatchEvent();
			}
		}
		this.signature = Long.valueOf(signature);
		this.maxLastModified = maxLastModified;
		this.itemCount = itemCount;
		
	}
	
	/**
	 * 監視状態をリセットする.<br>
	 * スレッドによりディレクトリを走査中であれば強制的に停止させる.<br>
	 * 検査結果をリセットして初回走査を行う.<br>
	 */
	public void reset() {
		if (Thread.currentThread() == thread) {
			throw new IllegalStateException();
		}
		synchronized (this) {
			if (thread != null) {
				thread.interrupt();
			}
		}
		try {
			semaphore.acquire();
			try {
				signature = null;
				watch();
			} finally {
				semaphore.release();
			}
		} catch (InterruptedException ex) {
			// 何もしない.
		}
	}
	
	/**
	 * イベントリスナを登録する
	 * @param l リスナ
	 */
	public void addPartsImageDirectoryWatchListener(PartsImageDirectoryWatchListener l) {
		if (l != null) {
			synchronized (listeners) {
				listeners.add(l);
			}
		}
	}
	
	/**
	 * イベントリスナを登録解除する
	 * @param l リスナ
	 */
	public void removePartsImageDirectoryWatchListener(PartsImageDirectoryWatchListener l) {
		if (l != null) {
			synchronized (listeners) {
				listeners.remove(l);
			}
		}
	}
	
	/**
	 * イベントを通知する.
	 */
	public void fireWatchEvent() {
		PartsImageDirectoryWatchListener[] listeners;
		synchronized (this.listeners) {
			listeners = this.listeners.toArray(new PartsImageDirectoryWatchListener[this.listeners.size()]);
		}
		PartsImageDirectoryWatchEvent e = new PartsImageDirectoryWatchEvent(this);
		for (PartsImageDirectoryWatchListener listener : listeners) {
			listener.detectPartsImageChange(e);
		}
	}
}
