/*
 * 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.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jp.sourceforge.mergedoc.pleiades.log.FileLogger;
import jp.sourceforge.mergedoc.pleiades.log.Logger;
import jp.sourceforge.mergedoc.pleiades.log.Logger.Level;
import jp.sourceforge.mergedoc.pleiades.resource.Files;
import jp.sourceforge.mergedoc.pleiades.resource.Mnemonics;
import jp.sourceforge.mergedoc.pleiades.resource.PatternCache;
import jp.sourceforge.mergedoc.pleiades.resource.Property;
import jp.sourceforge.mergedoc.pleiades.resource.PropertySet;
import jp.sourceforge.mergedoc.pleiades.resource.TranslationProperty;
import jp.sourceforge.mergedoc.pleiades.resource.TranslationString;

/**
 * 翻訳ルール・クラスです。
 * <p>
 * @author cypher256
 */
public class TranslationRule extends AbstractValidator {

	/** 分割ロガー */
	protected final Logger splitLog;

	/**
	 * 翻訳ルールを構築します。
	 * 検証結果は標準出力に出力されます。
	 * <p>
	 * @param existsProps 重複チェックに使用する既存プロパティー
	 */
	public TranslationRule(PropertySet... existsProps) {
		super(existsProps);
		splitLog = validationLog;
	}

	/**
	 * 翻訳ルールを構築します。
	 * <p>
	 * @param logFileName ログ・ファイル名。例) "props/validator.log"
	 * @param existsProps 重複チェックに使用する既存プロパティー
	 */
	public TranslationRule(String logFileName, PropertySet... existsProps) {

		super(logFileName, existsProps);

		String splitLogName = validationLogFileName.replace(".log", "_split.log");
		splitLog = Logger.getLogger("Split",
				FileLogger.class,
				Level.INFO,
				Files.conf(splitLogName));
	}

	/**
	 * ログ・ファイルのログ・レベルを取得します。
	 * <p>
	 * @return ログ・ファイルのログ・レベル
	 */
	@Override
	protected Level getLogFileLogLevel() {
		return Level.INFO;
	}

	/**
	 * プロパティーを句点解析により、。や . などで文を分割した後、
	 * 翻訳ルールを適用します。
	 * 分割されたエントリー分、数が増加します。
	 * <p>
	 * @param inProp プロパティー・セット
	 * @return ルール適用後のプロパティー
	 */
	public PropertySet apply(PropertySet inProp) {

		List<TranslationProperty> splitTpList = split(inProp);

		log.info("翻訳ルール適用中...");
		PropertySet outProp = new PropertySet();

		for (TranslationProperty tp : splitTpList) {
			applyEntry(tp, outProp);
		}
		log.info("%6d エントリー (適用後), %6d エントリー除去",
				outProp.size(), splitTpList.size() - outProp.size());

		return outProp;
	}

	/**
	 * プロパティーを句点解析により、。や . などで文を分割します。
	 * 分割されたエントリー分、数が増加します。
	 * このメソッドは分割するだけで trim は行われません。
	 * <p>
	 * @param inProp プロパティー・セット
	 * @return 分割後のプロパティー・セット
	 */
	protected List<TranslationProperty> split(PropertySet inProp) {

		log.info("句点解析分割中...");
		log.info("%6d エントリー (分割前)", inProp.size());

		List<TranslationProperty> splitTpList = new ArrayList<TranslationProperty>();
		List<TranslationProperty> noSplitTpList = new ArrayList<TranslationProperty>();

		for (Property p : inProp) {

			ValidationContext vc = new ValidationContext(p.key, p.value, null);
			TranslationProperty tp = new TranslationProperty(p);
			List<TranslationString> enList = tp.en.split();
			List<TranslationString> jaList = tp.ja.split();

			// 分割数が異なる場合は分割しない
			if (enList == null || jaList == null || enList.size() != jaList.size()) {
				noSplitTpList.add(tp);
				continue;
			}

			List<TranslationProperty> splitTempTpList = new ArrayList<TranslationProperty>();
			for (int i = 0; i < enList.size(); i++) {

				TranslationString eTs = enList.get(i);
				TranslationString jTs = jaList.get(i);

				// ここではトリムしない。apply メソッドで行う。
				String e = eTs.toString();
				String j = jTs.toString();

				// 対訳が不正な場合は句点解析不正のため分割しない
				Property iProp = getIllegalTranslationProperty(e, j);
				if (iProp != null) {
					vc.warn(
						"句点解析分割不可。「" + iProp.key +
						"」は「" + iProp.value + "」と対応している必要があります。\n" +
						Property.toString(e, j));
					break;
				}

				// {0} などの埋め込み引数が不正な場合は句点解析不正のため分割しない
				String iParam = getIllegalBindParameter(e, j);
				if (iParam != null) {
					vc.warn(
						"句点解析分割不可。訳文に含まれる " + iParam + " が原文にありません。\n" +
						Property.toString(e, j));
					break;
				}

				// [FORUM#79712] タグの対応不正 (チェック用にトリム)
				if (e.contains("<") || j.contains("<")) {

					String eTrim = eTs.trim();
					String jTrim = jTs.trim();

					String BLOCK_TAGS = "(?is).*<(div|p|ul|ol|dl|li|dd|h[1-5]|table|t[rdh]|html|body).*";
					boolean eTag = eTrim.matches(BLOCK_TAGS);
					boolean jTag = jTrim.matches(BLOCK_TAGS);

					if (eTag != jTag) {
						vc.warn("分割後のタグ対応不正 " + Property.toString(eTrim, jTrim));
						break;
					}
				}

				splitTempTpList.add(new TranslationProperty(eTs, jTs));
			}

			// エラーがなかった場合は分割したものを保管
			if (vc.isSuccess()) {

				splitTpList.addAll(splitTempTpList);

				// ログ
				StringBuilder sb = new StringBuilder();
				sb.append("句点解析分割: ");
				sb.append(enList.size());
				if (enList.size() != splitTempTpList.size()) {
					sb.append(" (");
					sb.append(enList.size() - splitTempTpList.size());
					sb.append(" 個の翻訳不要エントリー削除)");
				}
				for (TranslationProperty sp : splitTempTpList) {
					sb.append("\n" + sp);
				}
				splitLog.info(vc.formatMessage(sb.toString()));


			// 分割しない
			} else {

				noSplitTpList.add(tp);
			}
		}

		// apply メソッドは先勝ちのため、分割したものを後ろに追加
		noSplitTpList.addAll(splitTpList);
		splitTpList = noSplitTpList;

		log.info("%6d エントリー (分割後), %6d エントリー増加",
			splitTpList.size(), splitTpList.size() - inProp.size());

		return splitTpList;
	}

	/**
	 * 項目に翻訳ルールを適用します。
	 * @param tp 翻訳プロパティー
	 * @param outProp 出力プロパティー・セット
	 */
	protected void applyEntry(TranslationProperty tp, PropertySet outProp) {

		// トリムとニーモニックの除去
		String en = Mnemonics.removeEn(tp.en.trim());
		String ja = Mnemonics.removeJa(tp.ja.trim());
		ValidationContext vc = new ValidationContext(en, ja, null);

		// 空エントリー除去
		if (en.isEmpty() || ja.isEmpty()) {
			vc.warn("エントリーが空のため除外します。");
			return;
		}

		// 翻訳禁止は除去
		if (isForbidden(en)) {
			vc.warn("翻訳禁止のため除外します。");
			return;
		}

		//---------------------------------------------------------------------
		// ルール適用
		//---------------------------------------------------------------------

		// 検証無視セットに含まれる場合、そのまま追加
		if (vIgnoreSet.contains(en)) {
			outProp.put(en, ja);
			return;
		}

		// 訳語の統一のための置換（例：下さい→ください）
		for (Property rep : vTermProp) {

			String ng = rep.key;
			String ok = rep.value;

			if (ok.length() > 0 && ja.matches("(?s).*?" + ng + ".*")) {
				Pattern pat = PatternCache.get(ng);
				Matcher mat = pat.matcher(ja);
				StringBuffer sb = new StringBuffer();
				while (mat.find()) {mat.appendReplacement(sb, ok);}
				mat.appendTail(sb);
				ja = sb.toString();
			}
		}

		// 末尾の翻訳ルールを適用
		ja = optimizeSuffix(en, ja);

		// 英数字隣接空白を挿入
		for (Pattern pat : Validator.ADJACENT_WORD_SPACE_PATTERN_LIST) {
			ja = pat.matcher(ja).replaceAll("$1 $2");
		}

		// 英語リソースの複数形 (s) を除去
		if (!ja.contains("秒")) {
			en = TranslationString.removeS(en);
		}

		//---------------------------------------------------------------------
		// ルール適用後
		//---------------------------------------------------------------------

		// 未翻訳
		if (ja.equals(en)) {
			vc.info("ルール適用後、未翻訳のため除外します。");
			return;
		}

		// 既存辞書に異なる訳が存在する場合
		String jaExists = existsProp.get(en);
		if (jaExists != null && !jaExists.equals(ja)) {
			vc.error(
				"既: " + jaExists + "\n" +
				"既存辞書に異なる訳が存在します。");
			return;
		}

		// 同じ辞書に異なる訳が存在する場合
		jaExists = outProp.get(en);
		if (jaExists != null && !jaExists.equals(ja)) {
			vc.error(
				"既: " + jaExists + "\n" +
				"同じプロパティー・ファイル内に異なる訳が存在します。");
			return;
		}

		// 強制トリムし同じ訳が存在する場合
		//------------------------------------------------
		// ⇒ 2013/03/20 廃止。Misc.=その他 があっても Misc=その他 として保存され、
		//    実行時に Misc. → その他 → 句読点翻訳復元により その他。 になるため。
		// ⇒ 2016/07/16 Generator 保管前に処理
		/*
		String eForce = tp.en.trimForce();
		if (!en.equals(eForce)) {
			jaExists = existsProp.get(eForce);
			if (jaExists != null && jaExists.equals(ja)) {
				return;
			}
			jaExists = outProp.get(eForce);
			if (jaExists != null && jaExists.equals(ja)) {
				return;
			}
		}
		*/

		if (!tp.ja.toString().equals(ja)) {
			validationLog.info(
				"翻訳ルールにより最適化されました。\n      最適化前: %s\n      最適化後: %s\n",
				tp.getProperty(), Property.toString(en, ja));
		}

		outProp.put(en, ja);
	}

	/**
	 * 末尾文字を最適化します。
	 * <p>
	 * 原文末尾の「.」「:」「;」「,」に対し、訳文末尾「。」のような句点意訳を許容します。
	 * これはバリデーターの末尾チェックほど厳格ではありません。
	 * つまり、このメソッドで最適化しても、バリデーターでエラーとなる場合があり、
	 * 人による判断の余地を残しています。
	 * <p>
	 * @param en 英語
	 * @param ja 日本語
	 * @return 適用後の日本語
	 */
	protected String optimizeSuffix(String en, String ja) {

		// 訳文末尾の全角「：」を半角に
		if (ja.endsWith("：")) {
			ja = ja.replaceFirst("：$", ":");
		}
		// 訳文末尾の「:」前の空白除去
		if (!en.endsWith(" :") && ja.endsWith(" :")) {
			ja = ja.replaceFirst(" :$", ":");
		}

		// 原文末尾に「.」「:」「;」「,」が無い場合、訳文末尾の「。」を除去
		if (!en.endsWith(".") &&
			!en.endsWith(":") &&
			!en.endsWith(";") &&
			!en.endsWith(",") &&
			ja.endsWith("。")) {

			ja = ja.replaceFirst("。$", "");
		}
		// 原文末尾に「.」がある場合、訳文末尾に「。」を付加
		else if (!en.endsWith("..") && en.endsWith(".") && !ja.endsWith("。")) {

			// 1 単語の場合や途中に「:」が含まれる場合は付加しない
			if (en.contains(" ") && !en.contains(":") && !TranslationString.endsWithAbbrev(en)) {
				ja += "。";
			}
		}
		// 原文末尾に「:」がある場合、訳文末尾に「:」を付加
		else if (en.endsWith(":") && !ja.endsWith(":") && !ja.endsWith("。")) {
			ja += ":";
		}

		return ja;
	}

	/**
	 * メイン。
	 * 個別のプロパティー・ファイルに翻訳ルールを適用してみる場合の単独起動用です。
	 * <p>
	 * @param args
	 */
	public static void main(String... args) {

		// 翻訳ルール適用 (句点解析分割あり)
		String propPath = "props/_a.properties";
		PropertySet inProp = new PropertySet(propPath);
		PropertySet outProp = new TranslationRule(propPath + ".log").apply(inProp);

		// 検証
		PropertySet existsProp = new PropertySet(TRANS_PROP);
		new Validator("props/_a.properties.log", existsProp).validate(outProp);
		outProp.store("props/_a_out.properties", "");
	}
}
