/*
 * 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 static jp.sourceforge.mergedoc.pleiades.resource.FileNames.*;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;

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 org.apache.commons.io.FileUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.CharEncoding;

/**
 * 複数のプロパティー・ファイルをマージし、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%";

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

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

		String argStr = ArrayUtils.toString(args);
		log.info("起動オプション: " + argStr);
		isClean = argStr.contains("-clean");

		new Generator().run();
	}

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

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

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

		// 校正＆ルール適用済み言語パック辞書プロパティー (既存訳)
		PropertySet nlsUiCustomRuleProp = null;
		File nlsUriCustomDir = Files.getFile(NLS_UI_CUSTOM_DIR);
		File[] nlsUiPropFileList = PropertySet.listPropertiesFiles(nlsUriCustomDir);

		if (
			isClean ||
			isModified(TEMP_NLS_UI_CUSTOM_PROP, NLS_UI_CUSTOM_PROP) ||
			isModified(TEMP_NLS_UI_CUSTOM_PROP, nlsUiPropFileList)
		) {

			// 言語パックから取り込んだプロパティー・ファイルから既存にあるものを除去
			PropertySet transProp = new PropertySet(TRANS_PROP);

			for (File propFile : nlsUiPropFileList) {

				String propPath = NLS_UI_CUSTOM_DIR + "/" + propFile.getName();
				PropertySet prop = new PropertySet(propFile);
				prop = new TranslationRule(propPath + "_rule.log").apply(prop);

				// 既存に存在しないものだけを残す
				PropertySet newProp = new PropertySet();
				for (Property p : prop) {
					if (transProp.get(p.key) == null) {
						newProp.put(p);
					}
				}

				// 履歴付きで保管
				Generator.storeHistory(newProp, propPath,
					"eclipse.org 言語パック抽出・重複除去・校正済みプロパティー\n\n" +
					"  プラグイン別の言語パック辞書をマージし、校正したものです。\n" +
					"  オリジナル・ファイル：" + NLS_UI_CUSTOM_DIR + "/original/" + propFile.getName());

			}

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

			log.info("校正済み言語パック辞書プロパティーをロードします。");
			nlsUiCustomRuleProp = new PropertySet(NLS_UI_CUSTOM_DIR);
			nlsUiCustomRuleProp.load(NLS_UI_CUSTOM_PROP);

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

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

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

		} else {

			log.info("校正＆ルール適用済み言語パック辞書プロパティーをロードします。");
			nlsUiCustomRuleProp = new PropertySet(TEMP_NLS_UI_CUSTOM_PROP);
		}

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

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

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

		// 辞書の振り分け - ライセンス別のフォルダーごとに処理 (EPL 優先のため最後に処理)
		// EPL を最後にする
		File[] dirs = Files.getFile("props/additions").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.getFile("props/additions/regex-" + license + ".properties");
			File excludePropFile = Files.getFile("props/additions/exclude-" + license + ".properties");
			File transPropFile = Files.getFile("props/translation-" + license + ".properties");

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

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

			for (File propFile : propFiles) {
				if (propFile.lastModified() > transPropFile.lastModified()) {
					updated = true;
				}
			}

			if (updated || isClean) {

				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();
							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", nlsUiCustomRuleProp)
					.apply(transProp);

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

				// EPL の場合のマージ処理
				if (license.equals("EPL")) {
					transProp = mergeEPL(transProp, nlsUiCustomRuleProp);
				}

				// ライセンス別プロパティーの保管
				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);
		}


		//--------------------------------------------------------------------
		// マルチ・バイト・キー辞書の抽出
		//--------------------------------------------------------------------
		PropertySet outMultibyteProp = new PropertySet();
		for (Property p : outTransProp) {
			if (p.key.length() != p.key.getBytes().length) {
				outMultibyteProp.put(p);
			}
		}
		for (Property p : outMultibyteProp) {
			outTransProp.remove(p.key);
		}

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

		// 履歴を保存
		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) (秒を示すものではない場合)" +
			"");

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

		outExcludeProp.store(TRANS_EXCLUDE_PACKAGE_PROP,
			"翻訳除外パッケージ・プロパティー\n\n" +
			"  翻訳を Java パッケージ単位で除外訳するための辞書で、Pleiades が実行時に参照します。\n" +
			"  入力元ファイル：props/additions/*.properties のキー先頭に " + EXCLUDE_KEY_PREFIX + " がある項目");

		outMultibyteProp.store(TRANS_MULTIBYTES_KEY_PROP,
			"翻訳マルチ・バイト・キー・プロパティー\n\n" +
			"  翻訳プロパティーからキーにマルチ・バイトが含まれるものを抽出した辞書で、\n" +
			"  Pleiades が実行時に参照します。\n" +
			"  デフォルトの翻訳プロパティー・ロード抑止判定のために使用されます。");

		// ヘルプ HTML 辞書 (ルール適用済み、検証エラー含む)
		if (isClean || isModified(TRANS_HELP_UNCHECKED_PROP, TEMP_NLS_HELP_UNCHECKED_PROP)) {
			PropertySet outHelpUncheckedProp = new PropertySet(TEMP_NLS_HELP_UNCHECKED_PROP);
			outHelpUncheckedProp.store(TRANS_HELP_UNCHECKED_PROP,
				"ヘルプ辞書プロパティー (検証エラーのもの)\n\n" +
				"  言語パックのヘルプから抽出したものです。\n" +
				"  入力元ファイル：" + TEMP_NLS_HELP_UNCHECKED_PROP);
		}
	}

	/**
	 * EPL プロパティーをマージします。
	 * <p>
	 * @param eplProp EPL プロパティー
	 * @param nlsUiCustomRuleProp 校正＆ルール適用済み言語パック辞書プロパティー
	 * @return マージ済み EPL プロパティー
	 * @throws IOException 入出力k例外が発生した場合
	 */
	private PropertySet mergeEPL(PropertySet eplProp, PropertySet nlsUiCustomRuleProp) throws IOException {

		PropertySet mergedProp = new PropertySet();
		mergedProp.load(TEMP_NLS_HELP_CHECKED_PROP);	// ヘルプ未チェック辞書はマージしない
		mergedProp.putAll(eplProp);
		mergedProp.putAll(nlsUiCustomRuleProp);			// 既存言語パック優先のため最後に上書き

		// 分割なしプロパティー追加
		PropertySet noSplitProp = new PropertySet();
		noSplitProp.putAll(new TranslationRule().apply(noSplitProp));	// 先に分割したものを追加
		noSplitProp.load(NO_SPLIT_PROP);								// 未分割で上書き
		Validator v = new Validator(NO_SPLIT_PROP + "_validate.log", mergedProp);
		v.validate(noSplitProp);
		exitIfValidationError(v);

		mergedProp.putAll(noSplitProp);
		return mergedProp;
	}

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

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

	/**
	 * 入力ファイルの更新日付が新しいか判定します。
	 * <p>
	 * @param dstObj 出力ファイル
	 * @param srcs 入力ファイル
	 * @return 入力ファイルの更新日付が 1 つでも新しい場合は true
	 */
	private boolean isModified(Object dstObj, File... srcs) {
		File dst = (dstObj instanceof String) ? Files.getFile((String) dstObj) : (File) dstObj;
		for (File src : srcs) {
			if (src.lastModified() > dst.lastModified()) {
				return true;
			}
		}
		return false;
	}

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

		if (!validator.isSuccess()) {

			File logFile = Files.getFile(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.getFile(Files.toVcIgnoreName(path + ".txt"));
		if (textFile.exists()) {
			File textOldFile = Files.getFile(Files.toVcIgnoreName(path + "_old.txt"));
			textOldFile.delete();
			textFile.renameTo(textOldFile);
		}

		List<String> lines = new LinkedList<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);
	}
}
