package extensions;

import java.io.IOException;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import dareka.common.CloseUtil;
import dareka.common.Logger;
import dareka.common.Pair;
import dareka.extensions.CompleteCache;
import dareka.extensions.Extension2;
import dareka.extensions.ExtensionManager;
import dareka.processor.impl.Cache;
import dareka.processor.impl.CacheManager;
import dareka.processor.util.GetThumbInfoUtil;


/**
 * キャッシュの振り分けするよ
 */
public class cacheMoveExtension extends CacheManager implements Extension2, CompleteCache {
	
	public static final int REVISION = 170312;
	public static final String VER_STRING = "cacheMoveExtension_"+REVISION;
	
	private static ArrayList<Pair<Pattern, String>> cacheMoveRules;
	private static final File cacheMoveRulesFile = new File("local/cacheMoveRules.txt");
	private static volatile long cacheMoveRulesLastModified;
	
	// ログ表示用
	private static boolean isDebug() { return Boolean.getBoolean("cacheMoveDebug"); }
	private static void info(String mes) { Logger.info("cacheMove: ".concat(mes)); }
	private static void warn(String mes) { Logger.warning("cacheMove: ".concat(mes)); }
	private static void debug(String mes){ if(isDebug())_DBG(mes); }
	private static void _DBG(String mes) { Logger.info("cacheMove:[DEBUG] ".concat(mes)); }
	
	// Extension2 interface
	public void registerExtensions(ExtensionManager mgr) {
		mgr.registerCompleteCache(this);
	}
	
	public String getVersionString() {
		return VER_STRING;
	}
	
	// CompleteCache Interface
	public int getPriority() {
		// 先にcacheを参照できなければ優先度の動的変更は意味無いような？
		return (cacheMoveRules != null) ? Integer.MAX_VALUE : -1;
	}
	
	public void update() {
		long lastmod = cacheMoveRulesFile.lastModified();
		if (cacheMoveRulesLastModified < lastmod) {
			cacheMoveRulesLastModified = lastmod;
			loadCacheMoveRules();
		}
	}
	
	public boolean onComplete(Cache cache) throws IOException {
		String smid = cache.getVideoId();
		String thumbinfo = GetThumbInfoUtil.getWithRetry(smid, 3, 3000);
		if (thumbinfo != null) {
			for (Pair<Pattern, String> e : cacheMoveRules) {
				Matcher m = e.first.matcher(thumbinfo);
				if (m.find()) {
					// グループ文字列($1とか)を置換する
					String folder = e.second;
					for (int n = 0; n <= m.groupCount(); n++) {
						folder = folder.replaceAll("\\$"+n+"(?!\\d)", m.group(n));
					}
					if (folder.endsWith("/")) {
						folder = folder.substring(0, folder.length());
					}
					if (existsFolder(folder) || makeFolder(folder)) {
						info(folder);
						boolean moved = cache.moveTo(folder);
						if(!moved) warn("move failed.");
						return moved;
					}
				}
			}
		} else {
			warn("getthumbinfo failed.");
		}
		return false; // NOT MOVED
	}
	
	private synchronized void loadCacheMoveRules() {
		ArrayList<Pair<Pattern, String>> rules = new ArrayList<Pair<Pattern, String>>();
		BufferedReader br = null;
		try {
			br = new BufferedReader(new InputStreamReader(
					new FileInputStream(cacheMoveRulesFile), "UTF-8"));
			String line, folder = null;
			while ((line = br.readLine()) != null) {
				if (line.isEmpty() || line.startsWith("#")) {
					continue;
				} else if (line.startsWith("=")) {
					folder = line.substring(1);
				} else {
					parseLine(rules, line, folder);
				}
			}
			cacheMoveRules = rules; // 読み込んだルールで更新
			debug("loaded: " + cacheMoveRulesFile.getPath());
		} catch (IOException e) {
			Logger.error(e);
			warn("load failed: " + cacheMoveRulesFile.getPath());
		} finally {
			CloseUtil.close(br);
		}
	}
	
	private static final Pattern RULES_LINE_PATTERN = Pattern.compile(
			"^([^\\t]+)(\\t+([^\\t].*))?$");
	private void parseLine(ArrayList<Pair<Pattern, String>> rules, String line, String folder) {
		Matcher m = RULES_LINE_PATTERN.matcher(line);
		if (m.matches()) {
			String pat = m.group(1);
			if (m.group(2) != null) {
				folder = m.group(3);
			}
			if (pat.startsWith("$") && (pat = expandMacro(pat)) == null) {
				warn("macro error: " + line);
				return;
			}
			try {
				rules.add(new Pair<Pattern, String>(Pattern.compile(pat), folder));
				debug("loaded " + pat + " => " + folder);
			} catch (PatternSyntaxException e) {
				warn("regex error: " + line);
			}
		}
	}
	
	private static final Pattern RULES_MACRO_PATTERN = Pattern.compile(
			"^\\$([A-Z_]+)(\\(.+\\))$");
	private String expandMacro(String pat) {
		Matcher m = RULES_MACRO_PATTERN.matcher(pat);
		if (m.matches()) {
			String name = m.group(1), param = m.group(2);
			if ("CATEGORY".equals(name)) {
				return "<tag [^>]*category=\"1\"[^>]*>(?i:"+param+")</tag>";
			} else if ("SOTITLE".equals(name)) {
				return "<video_id>so\\d+</video_id>[\\s\\S]+<title>"+param+"</title>";
			} else if ("TAG".equals(name)) {
				return "<tag[^>]*>(?i:"+param+")</tag>";
			} else if ("TAGLOCK".equals(name)) {
				return "<tag [^>]*lock=\"1\"[^>]*>(?i:"+param+")</tag>";
			} else if ("TITLE".equals(name)) {
				return "<title>"+param+"</title>";
			} else if ("USERID".equals(name)) {
				return "<user_id>"+param+"</user_id>";
			}
		}
		return null; // FAILURE
	}
	
	private boolean existsFolder(String folder) {
		for (String d : dirList) {
			if(d.equals(folder)) return true;
		}
		debug("folder '" + folder + "' not exists.");
		return false;
	}
	
	private boolean makeFolder(String path) {
		String parent = path, folder = null;
		while (folder == null) {
			// 親フォルダを辿って存在するならFileを取得(.folder対策)
			int pos = parent.lastIndexOf("/");
			if(pos == -1) break;
			parent = parent.substring(0, pos);
			File d = dir2File.get(parent);
			if (d != null) {
				folder = path.replace(parent, d.getAbsolutePath());
			}
		}
		if (folder == null) {
			folder = getCacheDir() + "/" + path;
		}
		File folderFile = new File(folder);
		if (!folderFile.exists()) {
			debug("mkdirs of '" + folder + "'...");
			if (!folderFile.mkdirs()) {
				warn("mkdirs failed: " + folderFile.getAbsolutePath());
			}
		}
		if (folderFile.isDirectory()) {
			// dirListに無ければ追加してmoveTo()できるようにする
			if (!dirList.contains(path)) {
				dirList.add(path);
				dir2File.put(path, folderFile);
			}
		} else {
			warn("not a directory: " + folderFile.getAbsolutePath());
		}
		return existsFolder(path);
	}
	
}
