/*
 * Copyright (c) 2005- Shinji Kashihara.
 * All rights reserved. This program are made available under
 * the terms of the Eclipse Public License v1.0 which accompanies
 * this distribution, and is available at epl-v10.html.
 */
package jp.sourceforge.mergedoc.pleiades.generator;

import jp.sourceforge.mergedoc.org.apache.commons.io.FileUtils;
import jp.sourceforge.mergedoc.org.apache.commons.lang.ArrayUtils;
import jp.sourceforge.mergedoc.org.apache.commons.lang.CharEncoding;
import jp.sourceforge.mergedoc.pleiades.log.Logger;
import jp.sourceforge.mergedoc.pleiades.resource.Files;
import jp.sourceforge.mergedoc.pleiades.resource.Property;
import jp.sourceforge.mergedoc.pleiades.resource.PropertySet;
import jp.sourceforge.mergedoc.pleiades.resource.TranslationString;
import jp.sourceforge.mergedoc.pleiades.runtime.resource.RegexDictionary;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;

import static jp.sourceforge.mergedoc.pleiades.resource.FileNames.*;

/**
 * 複数のプロパティー・ファイルをマージし、1 つのプロパティー・ファイル
 * (translation.properties) を作成するための翻訳プロパティー・ジェネレーターです。
 * <p>
 * @author cypher256
 */
public class Generator {

	/** ロガー */
	private static final Logger log = Logger.getLogger(Generator.class);

	/** 正規表現キープレフィックス */
	private static final String REGEX_KEY_PREFIX = "%REGEX%";

	/** 翻訳除外キープレフィックス */
	private static final String EXCLUDE_KEY_PREFIX = "%EXCLUDE%";

	/** パッケージ条件キープレフィックス */
	private static final String IF_KEY_PREFIX = "%IF%";

	/** 校正＆ルール適用済み言語パック辞書の再構築を行う場合は true */
	private static boolean isClean;

	/**
	 * ジェネレーターを開始するための main メソッドです。
	 * <p>
	 * @param args 起動引数
	 * @throws IOException 入出力例外が発生した場合
	 */
	public static void main(String... args) throws IOException {

		String argStr = ArrayUtils.toString(args);
		isClean = argStr.contains("-clean");

		new Generator().run();
	}

	/**
	 * ジェネレーターを実行します。
	 * <p>
	 * @throws IOException 入出力例外が発生した場合
	 */
	private void run() throws IOException {

		// ログ削除
		for (File logFile : Files.conf("props").listFiles(Files.createSuffixFilter(".log"))) {
			logFile.delete();
		}

		//--------------------------------------------------------------------
		// 言語パック辞書のロード
		//--------------------------------------------------------------------

		// 校正＆ルール適用済み Babel プロパティー (既存訳)
		PropertySet babelProp = null;
		boolean updatedFile = false;

		if (!isClean) {
			if (isModified(TRANS_PROP, BABEL_CUSTOM_PROP, HELP_CUSTOM_PROP) ||
				isModified(TRANS_PROP, Files.conf("props").listFiles(Files.createFileFilter()))
			) {
				log.info("Babel プロパティー・ファイルの更新を検出したため -clean で実行します。");
				isClean = true;
				updatedFile = true;
			}
		}

		if (isClean) {

			// TranslationRule で英文から & 除去。ニーモニックでなくても除去。&1 など。
			// これは実際に翻訳されるときに lookup メソッドで英文からまず & が除去されるため。
			// translation.properties の英文は & が付かない形式で格納されることに注意。
			// 日本語のほうはかっこがない限り除去されることはない。

			log.info("校正済み Babel 言語パック・プロパティーをロードします。");
			babelProp = new PropertySet(BABEL_CUSTOM_PROP);

			log.info("校正済み Babel 言語パック・プロパティーに翻訳ルールを適用分割中...");
			babelProp = new TranslationRule(BABEL_CUSTOM_PROP + "_rule.log").apply(babelProp);
			babelProp.store(TEMP_BABEL_CUSTOM_PROP,
				"校正＆ルール適用済み言語パック・プロパティー (検証前一時ファイル)");

			// 現状、検証は分割後に最適化されているため、分割後に行わないと末尾 .. などでエラーとなる
			log.info("校正済み Babel 言語パック・プロパティーの問題除去中 (翻訳ルール適用分割後) ...");
			Validator v = new Validator(BABEL_CUSTOM_PROP + "_validate.log");
			v.validate(babelProp);
			exitIfValidationError(v);

			babelProp.store(TEMP_BABEL_CUSTOM_PROP, "校正＆ルール適用済み言語パック・プロパティー");

		} else {

			log.info("校正＆ルール適用済み Babel プロパティーをロードします。");
			babelProp = new PropertySet(TEMP_BABEL_CUSTOM_PROP);
		}

		//--------------------------------------------------------------------
		// 追加辞書のロード
		//--------------------------------------------------------------------

		// 辞書プロパティー
		PropertySet outTransProp = new PropertySet();
		// 正規表現辞書プロパティー
		PropertySet outRegexProp = new PropertySet();
		// 除外プロパティー
		PropertySet outExcludeProp = new PropertySet();

		// バリデーター
		Validator v = new Validator(TEMP_ALL_PROP + "_validate.log", babelProp);

		// 辞書の振り分け - ライセンス別のフォルダーごとに処理 (EPL 優先のため最後に処理)
		// EPL を最後にする
		File[] dirs = Files.conf("props/source").listFiles(Files.createDirectoryFilter());
		Arrays.sort(dirs, new Comparator<File>(){
			public int compare(File dir1, File dir2) {
				String dir1Name = dir1.getName();
				return dir1Name.equals("EPL") ? 1 : dir1Name.compareTo(dir2.getName());
			}
		});

		for (File dir : dirs) {

			String license = dir.getName();
			if (license.startsWith(".")) {
				continue;
			}

			File regexPropFile = Files.conf("props/source/regex-" + license + ".properties");
			File excludePropFile = Files.conf("props/source/exclude-" + license + ".properties");
			File transPropFile = Files.conf("props/translation-" + license + ".properties");

			PropertySet regexProp = new PropertySet();
			PropertySet excludeProp = new PropertySet();
			PropertySet transProp = new PropertySet();

			File[] propFiles = dir.listFiles(Files.createSuffixFilter(".properties"));
			boolean updatedPerLicense = false;

			if (!isClean) {
				if (isModified(TRANS_PROP, propFiles)) {
					updatedPerLicense = true;
					updatedFile = true;
					log.info("プロパティー・ファイル更新を検出しました。" + license);
				}
			}
			if (isClean || updatedPerLicense) {

				Arrays.sort(propFiles); // 昇順
				for (File propFile : propFiles) {

					PropertySet addProp = new PropertySet(propFile);
					log.debug("Loaded %s %d", propFile.getName(), addProp.size());

					// % で始まる特殊プロパティを抽出し、別プロパティへ振り分け
					for (Property p : addProp) {

						if (p.key.startsWith(REGEX_KEY_PREFIX)) {

							// 正規表現プロパティ
							String newKey = p.key.replaceFirst("^" + REGEX_KEY_PREFIX, "").trim();
							String newVal = p.value.trim();

							if (newKey.endsWith(".") && !newKey.endsWith("\\.")) {
								log.warn("正規表現不正 - 末尾ドット未エスケープ: " + propFile.getName() + ": " + newKey);
							}
							if (newKey.endsWith("?") && !newKey.endsWith("\\?")) { // 正規表現として使用不可
								log.error("正規表現不正 - 末尾 ? 未エスケープ: " + propFile.getName() + ": " + newKey);
								System.exit(-1);
							}
							if (newKey.startsWith("(?s)")) {
								log.warn("正規表現不正 - DOTALL (?s) フラグ除去: " + propFile.getName() + ": " + newKey);
								newKey = newKey.substring(4);
							}
							regexProp.put(newKey, newVal);

						} else if (p.key.startsWith(EXCLUDE_KEY_PREFIX)) {

							// 翻訳除外プロパティ
							String newKey = p.key.replaceFirst("^" + EXCLUDE_KEY_PREFIX, "").trim();
							Object existsValue = outExcludeProp.get(newKey);
							if (existsValue != null) {
								p.value = existsValue + "," + p.value;
							}
							excludeProp.put(newKey, p.value);

						} else {

							// 訳語の検証
							v.validate(p, propFile.getName());

							// 翻訳辞書
							transProp.put(p);
						}
					}
				}

				v.logEndMessage("ライセンス別翻訳プロパティー (" + license + ") 検証結果");
				exitIfValidationError(v);

				log.info("ライセンス別翻訳プロパティー (" + license + ") に翻訳ルールを適用分割中...");
				transProp = new TranslationRule(TEMP_ALL_PROP + "_rule.log", babelProp).apply(transProp);

				log.info("ライセンス別翻訳プロパティー (" + license + ") の問題除去中 (翻訳ルール適用分割後) ...");
				v = new Validator(TEMP_ALL_PROP + "_validate.log", babelProp);
				transProp = v.remove(transProp);
				exitIfValidationError(v);

				// EPL の場合のマージ処理
				if (license.equals("EPL")) {

					PropertySet mergedProp = new PropertySet();
					mergedProp.load(HELP_CUSTOM_PROP);	// ヘルプ未チェック辞書はマージしない
					mergedProp.putAll(transProp);
					mergedProp.putAll(babelProp);	// 校正済み Babel 言語パック優先のため最後に上書き
					transProp = mergedProp;
				}

				// ライセンス別プロパティーの保管
				transProp.store(transPropFile, "ライセンス別翻訳プロパティー (" + license + ")");
				regexProp.store(regexPropFile, "ライセンス別正規表現翻訳プロパティー (" + license + ")");
				excludeProp.store(excludePropFile, "ライセンス別翻訳除外プロパティー (" + license + ")");

			} else {

				// ライセンス別プロパティーをロード
				transProp.load(transPropFile);
				regexProp.load(regexPropFile);
				excludeProp.load(excludePropFile);
			}

			outTransProp.putAll(transProp);
			outRegexProp.putAll(regexProp);
			outExcludeProp.putAll(excludeProp);
		}

		if (!updatedFile && !isClean) {
			log.info("更新なし");
			return;
		}

		//--------------------------------------------------------------------
		// パッケージ条件プロパティの抽出 (書式 %IF%aaa.bbb>英語=日本語 )
		//--------------------------------------------------------------------
		Map<String, PropertySet> pkgConditionMap = new HashMap<String, PropertySet>();
		List<String> pkgRemoveKeys = new ArrayList<String>();

		for (Property p : outTransProp) {

			if (!p.key.startsWith(IF_KEY_PREFIX)) {
				continue;
			}
			pkgRemoveKeys.add(p.key);

			String check = p.key + p.value;
			if (check.contains(",") || check.contains("=")) {
				throw new IllegalStateException("[,=] 使用不可: " + p);
			}
			String[] key = p.key.replaceFirst("^" + IF_KEY_PREFIX, "").trim().split(">");
			if (key.length != 2) {
				throw new IllegalStateException("区切り文字 '>' 欠落 " + p);
			}
			String pkg = key[0];
			String en = key[1];
			PropertySet pkgJaMap = pkgConditionMap.get(en);
			if (pkgJaMap == null) {
				pkgJaMap = new PropertySet();
				pkgConditionMap.put(en, pkgJaMap);
			}
			pkgJaMap.put(pkg, p.value);
		}
		for (String key : pkgRemoveKeys) {
			outTransProp.remove(key);
		}

		//--------------------------------------------------------------------
		// パッケージ条件プロパティの集約
		//--------------------------------------------------------------------
		for (Entry<String, PropertySet> pkgConditionEntry : pkgConditionMap.entrySet()) {

			String en = pkgConditionEntry.getKey();
			String jaDefault = outTransProp.get(en);

			StringBuilder sb = new StringBuilder();
			sb.append(IF_KEY_PREFIX);
			sb.append(jaDefault);
			sb.append(",");

			for (Property pkgJa : pkgConditionEntry.getValue()) {
				sb.append(pkgJa.key);
				sb.append("=");
				sb.append(pkgJa.value);
				sb.append(",");
			}
			String pkgValues = sb.toString().replaceFirst(",$", "");

			outTransProp.put(en, pkgValues);
			log.info("パッケージ条件プロパティ: " + en + "=" + pkgValues);
			if (jaDefault == null) {
				throw new IllegalStateException("パッケージ条件プロパティのデフォルトが定義されていません。");
			}
		}

		//--------------------------------------------------------------------
		// 優先プロパティーの抽出
		//--------------------------------------------------------------------
		log.info("優先プロパティーを抽出します。");
		PropertySet outFirstProp = new PropertySet();
		RegexDictionary regex = RegexDictionary.getInstance();

		for (Property p : outTransProp) {

			// マルチバイトキー
			if (p.key.length() != p.key.getBytes().length) {
				outFirstProp.put(p);
			}
			else {
				// 正規表現に一致する場合も優先 (正規表現プロパティーがデフォルトより先に参照されるため)
				String enRegexResult = regex.lookup(p.key);
				if (enRegexResult != null) {
					outFirstProp.put(p);
					log.debug("--------------------------------------------------------------");
					log.debug("通常翻訳: %s [%s]", p.value, p.key);
					log.debug("正規表現: %s", enRegexResult);
				}
			}
		}

		// 分割なしプロパティー追加 (正規表現より優先するため)
		PropertySet noSplitProp = new PropertySet(NO_SPLIT_PROP);
		Validator noSpV = new Validator(NO_SPLIT_PROP + "_validate.log", outTransProp);
		noSpV.validate(noSplitProp);
		exitIfValidationError(noSpV);
		outFirstProp.putAll(noSplitProp);

		// 分割なしプロパティーを分割したものを追加 (トリムされた訳定義に対応、存在する場合は追加しない)
		for (Property p : new TranslationRule().apply(noSplitProp)) {
			if (!outTransProp.containsKey(p.key)) {
				outTransProp.put(p);
			}
		}

		// 優先プロパティにあるものは通常から削除
		for (Property p : outFirstProp) {
			outTransProp.remove(p.key);
		}

		//--------------------------------------------------------------------
		// 強制トリムして同じ訳が有る場合は除去、無い場合は強制トリム済みを格納
		// → 末尾が ? や : などで違う訳の場合は残す
		//--------------------------------------------------------------------
		log.info("強制トリムします。");
		int dupl = 0;
		int same = 0;
		int diff = 0;
		int repl = 0;

		for (Property p : new PropertySet(outTransProp)) {

			String enTrim = new TranslationString(p.key).trimForce();
			String jaTrim = new TranslationString(p.value).trimForce();

			if (v.isForbidden(enTrim)) {
				continue;
			}
			if (enTrim.equals(jaTrim)) {
				outTransProp.remove(p.key);
				same++;
			}
			// 強制トリム可能なエントリー (Misc.=その他 などに対応するため ja も判定)
			else if (!p.key.equals(enTrim) && !p.value.equals(jaTrim)) {
				String jaOther = outTransProp.get(enTrim);

				// 強制トリム後のエントリーがすでに有る場合
				if (jaOther != null) {

					TranslationString jaTsDup = new TranslationString(jaOther);
					String jaTrimOther = jaTsDup.trimForce();

					// 強制トリム後の訳が同じものが有る場合 ⇒ トリム前エントリーを削除
					if (jaTrimOther.equals(jaTrim)) {
						outTransProp.remove(p.key);
						dupl++;
					} else {
						diff++;
					}
				}
				// 強制トリム後のエントリーが無い場合 ⇒ 強制トリム後に置換
				else {
					outTransProp.remove(p.key);
					outTransProp.put(enTrim, jaTrim);
					repl++;
				}
			}
		}
		log.info("強制トリム (既存重複削除:%d, キーと値同一削除:%d, 相違のため変更無し:%d, トリム後に置換:%d)",
			dupl, same, diff, repl);

		//--------------------------------------------------------------------
		// 保管
		//--------------------------------------------------------------------

		// 最大長確認 (Validator.EN_MAX_LENGTH に関連)
		int maxLen = 0;
		for (Property p : outTransProp) {
			maxLen = Math.max(maxLen, p.key.length());
		}
		log.info("英語最大長: " + maxLen);

		// 履歴を保存
		storeHistory(outTransProp, TEMP_ALL_PROP, "辞書全プロパティー（生成確認用テンポラリー）");

		outTransProp.store(TRANS_PROP,
			"翻訳辞書プロパティー\n\n" +
			"  ライセンス別翻訳プロパティーをマージしたもので、\n" +
			"  Pleiades が実行時に参照します。\n" +
			"  \n" +
			"  入力元ファイル：props/translation-*.properties\n" +
			"  \n" +
			"  句点解析によりエントリーは可能な限り文単位に分割されています。また、\n" +
			"  重複防止やリソース、メモリー消費量低減のため、次のような文字は\n" +
			"  除去されており、翻訳時は原文を元に自動的に補完されます。\n" +
			"  \n" +
			"  ・ニーモニック：英語の場合は & 1 文字、日本語の場合は (&x) 4 文字\n" +
			"  ・先頭・末尾の連続する空白：\\r、\\n、\\t、半角スペース\n" +
			"  ・前後の囲み文字 ()、[]、<>、\"\"、''、!! など (前後の組み合わせが一致する場合)\n" +
			"  ・先頭の IWAB0001E のような Eclipse 固有メッセージ・コード\n" +
			"  ・複数形を示す (s) (秒を示すものではない場合)" +
			"");

		// 次回実行時のファイル更新日付判定用に更新
		if (updatedFile) {
			Files.conf(TRANS_PROP).setLastModified(System.currentTimeMillis());
		}

		outRegexProp.store(TRANS_REGEX_PROP,
			"正規表現辞書プロパティー\n\n" +
			"  正規表現で翻訳するための辞書で、Pleiades が実行時に参照します。\n" +
			"  常に DOTALL モードで動作するため定義では先頭の (?s) は不要です。\n" +
			"  入力元ファイル：props/source/*.properties のキー先頭に " + REGEX_KEY_PREFIX + " がある項目");

		outExcludeProp.store(TRANS_EXCLUDE_PACKAGE_PROP,
			"翻訳除外パッケージ・プロパティー\n\n" +
			"  翻訳を呼び出し元階層 Java パッケージ単位で除外訳するための辞書で、Pleiades が実行時に参照します。\n" +
			"  xml 定義の excludeTrace と異なり advice に ?{JOINT_POINT} 指定は不要です。\n" +
			"  入力元ファイル：props/source/*.properties のキー先頭に " + EXCLUDE_KEY_PREFIX + " がある項目");

		outFirstProp.store(TRANS_FIRST_PROP,
			"翻訳優先プロパティー\n\n" +
			"  翻訳プロパティーからキーにマルチ・バイトが含まれるもの、および正規表現辞書に一致するエントリーを\n" +
			"  抽出した辞書で、Pleiades が実行時に正規表現辞書より先に参照します。\n" +
			"  デフォルトの翻訳プロパティー・ロード抑止判定のために使用されます。");
	}

	/**
	 * 入力ファイルの更新日付が新しいか判定します。
	 * <p>
	 * @param dstObj 出力ファイル
	 * @param srcs 入力ファイル
	 * @return 入力ファイルの更新日付が 1 つでも新しい場合は true
	 */
	private boolean isModified(Object dstObj, String... srcs) {

		List<File> list = new ArrayList<File>();
		for (String src : srcs) {
			list.add(Files.conf(src));
		}
		File[] srcArray = list.toArray(new File[list.size()]);
		File dstFile = (dstObj instanceof String) ? Files.conf((String) dstObj) : (File) dstObj;
		return isModified(dstFile, srcArray);
	}

	/**
	 * 入力ファイルの更新日付が新しいか判定します。
	 * <p>
	 * @param dstObj 出力ファイル
	 * @param srcs 入力ファイル
	 * @return 入力ファイルの更新日付が 1 つでも新しい場合は true
	 */
	private boolean isModified(Object dstObj, File... srcs) {
		File dstFile = (dstObj instanceof String) ? Files.conf((String) dstObj) : (File) dstObj;
		for (File src : srcs) {
			if (src.lastModified() > dstFile.lastModified()) {
				log.info("入力ファイル更新検出 %s", src.getName());
				log.info("更新時刻 (出力): %1$tF %1$tT - %2$s", dstFile.lastModified(), dstFile.getName());
				log.info("更新時刻 (入力): %1$tF %1$tT - %2$s", src.lastModified(), src.getName());
				return true;
			}
		}
		return false;
	}

	/**
	 * バリデーターにエラーがある場合、強制終了します。
	 * (Ant からの呼び出しを終了させるため強制終了)
	 * <p>
	 * @param validator バリデーター
	 * @throws IOException 入出力k例外が発生した場合
	 */
	private void exitIfValidationError(Validator validator) throws IOException {

		if (!validator.isSuccess()) {

			File logFile = Files.conf(validator.getLogFileName());
			String s = FileUtils.readFileToString(logFile, CharEncoding.UTF_8);
			log.info(s);

			log.error("検証エラーのため、強制終了します。");
			System.exit(-1);
		}
	}

	/**
	 * プロパティーを保管し、diff 用にテキスト・ファイルとして 2 世代保管します。
	 * <p>
	 * @param prop プロパティー
	 * @param path 保管パス
	 * @param comment コメント文字列
	 * @throws IOException 入出力k例外が発生した場合
	 */
	public static void storeHistory(PropertySet prop, String path, String comment) throws IOException {

		List<String> keyList = prop.store(path, comment);
		if (keyList == null) {
			return;
		}

		File textFile = Files.conf(Files.toVcIgnoreName(path + ".txt"));
		if (textFile.exists()) {
			File textOldFile = Files.conf(Files.toVcIgnoreName(path + "_old.txt"));
			textOldFile.delete();
			textFile.renameTo(textOldFile);
		}

		List<String> lines = new ArrayList<String>();
		lines.add("#" + prop.size() + " エントリー");
		for (String key : keyList) {
			String line = Property.toString(key, prop.get(key));
			lines.add(line);
		}
		FileUtils.writeLines(textFile, CharEncoding.UTF_8, lines);
	}
}
