/*
 * 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.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jp.sourceforge.mergedoc.pleiades.resource.Property;
import jp.sourceforge.mergedoc.pleiades.resource.PropertySet;
import jp.sourceforge.mergedoc.pleiades.resource.TranslationString;

/**
 * 翻訳バリデーター・クラスです。
 * 訳語を検証し、結果をログ出力します。
 * <p>
 * @author cypher256
 */
public class Validator extends AbstractValidator {

	/** 検証済みのプロパティーのマップ (キー：英語文字列) */
	private final Map<String, ValidationContext> validatedMap = new HashMap<String, ValidationContext>();

	/** 翻訳スルー正規表現セット */
	private final Set<String> regexThroughSet = new PropertySet(TRANS_REGEX_THROUGH_PROP).keySet();

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

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

	/**
	 * 訳語を検証し、エラー・警告があるエントリーを除いた
	 * 新しいプロパティー・セットを作成します。
	 * <p>
	 * @param prop プロパティー・セット
	 * @return エラー・警告を除去した新しいプロパティー・セット
	 */
	public PropertySet remove(PropertySet prop) {

		PropertySet outProp = new PropertySet();
		for (Property p : prop) {
			if (validate(p).isSuccess()) {
				outProp.put(p);
			}
		}
		logEndMessage("検出されたエラー・警告を削除しました。");

		return outProp;
	}

	/**
	 * 訳語を検証します。
	 * <p>
	 * @param prop プロパティー・セット
	 */
	public void validate(PropertySet prop) {

		for (Property p : prop) {
			validate(p);
		}
		logEndMessage("検証結果");
	}

	/**
	 * 訳語を検証します。
	 * <p>
	 * @param p プロパティー
	 * @return 検証結果
	 */
	public ValidationResult validate(Property p) {
		return validate(p, "");
	}

	/**
	 * 訳語を検証します。
	 * <p>
	 * @param p プロパティー
	 * @param name ファイル名などの識別子
	 * @return 検証結果
	 */
	public ValidationResult validate(Property p, String name) {

		// トリム
		String en = TranslationString.trim(p.key);
		String ja = TranslationString.trim(p.value);
		ValidationContext vc = createValidationContext(en, ja, name);

		// 検証無視セットに含まれる場合、正しいと判断
		if (vIgnoreSet.contains(en)) {
			return vc;
		}

		//---------------------------------------------------------------------
		// 翻訳忘れチェック
		//---------------------------------------------------------------------
		if (en.length() == 0 || ja.length() == 0) {
			vc.warn("エントリーが空です。");
			return vc;
		}
		if (ja.equals(en)) {
			vc.warn("翻訳されていません。");
			return vc;
		}

		//---------------------------------------------------------------------
		// 翻訳禁止チェック
		//---------------------------------------------------------------------

		if (isForbidden(en)) {
			vc.error("この訳は翻訳が許可されていません。このエントリーを削除してください。");
			return vc;
		}

		//---------------------------------------------------------------------
		// 言語パック辞書重複チェック
		//---------------------------------------------------------------------
		String jaExists = existsProp.get(en);
		if (jaExists != null) {

			if (jaExists.equals(ja)) {

				vc.info(
					"既存の校正済み言語パック辞書に同一の訳が存在します。");

			} else {

				vc.error(
					"既: " + jaExists + "\n" +
					"既存の校正済み言語パック辞書に異なる訳が存在します。" +
					"既存訳を確認してください。");
			}
		}

		//---------------------------------------------------------------------
		// 追加辞書同士の重複チェック
		//---------------------------------------------------------------------
		ValidationContext validatedVc = validatedMap.get(en);
		if (validatedVc != null) {

			if (ja.equals(validatedVc.ja)) {

				if (name.equals(validatedVc.name)) {
					vc.info(
						"翻訳トリムすると、同じプロパティー・ファイル内に同じ訳が存在します。");
				} else {
					vc.info(
						"別のプロパティー " + validatedVc.name + " に同じ訳が既に存在します。");
				}

			} else {

				if (name.equals(validatedVc.name)) {

					vc.error(
						"異: " + Property.escapeValue(validatedVc.ja) + "\n" +
						"翻訳トリムすると、同じプロパティー・ファイル内に異なる訳が存在します。" +
						"訳を統一してください。");
				} else {
					vc.error(
						"異: " + Property.escapeValue(validatedVc.ja) + "\n" +
						"別のプロパティーに異なる訳が既に存在します。" + validatedVc.name + "");
				}
			}
		}
		validatedMap.put(en, vc);

		//---------------------------------------------------------------------
		// 末尾の同一性チェック
		//---------------------------------------------------------------------

		validateSuffix(en, ja, vc);

		//---------------------------------------------------------------------
		// 先頭の同一性チェック
		//---------------------------------------------------------------------

		// 先頭の「=」チェック --- 例) xxx:=xxx のような : のエスケープもれ検証
		if (ja.startsWith("=") && !en.contains("=")) {
			vc.error("訳文の先頭に不要な「=」があります。");
		}

		//---------------------------------------------------------------------
		// 英数字隣接空白チェック
		//---------------------------------------------------------------------

		for (Pattern pat : ADJACENT_WORD_SPACE_PATTERN_LIST) {
			Matcher mat = pat.matcher(ja);
			while (mat.find()) {
				vc.error("英数字に隣接する全角文字の間に半角スペースが必要です。→"
						+ mat.group() + "");
			}
		}

		//---------------------------------------------------------------------
		// {0} などの埋め込み引数の出現数チェック
		//---------------------------------------------------------------------

		String iParam = getIllegalBindParameter(en, ja);
		if (iParam != null) {
			vc.error("原文に含まれる " + iParam + " が訳文にも必要です。");
		}

		//---------------------------------------------------------------------
		// 改行数チェック
		//---------------------------------------------------------------------

		//// フォーマット済みテキスト (改行の後に \\s がある) の場合のみ
		// → 将来、改行自動最適化を実装した場合に必要
		//if (TranslationString.isFormatedText(en)) {
		int eCount = en.length() - en.replace("\n", "").length();
		int jCount = ja.length() - ja.replace("\n", "").length();
		if (eCount < 10 && eCount != jCount) {

			// trim 前で再検査し、OK であれば検証 OK とする
			eCount = p.key.length()   - p.key.replace("\n", "").length();
			jCount = p.value.length() - p.value.replace("\n", "").length();

			if (eCount < 10 && eCount != jCount) {
				vc.error("改行文字 \\n の数が異なります。原文:" + eCount + " 訳文:" + jCount + "");
			}
		}

		//---------------------------------------------------------------------
		// 二重クォート数チェック
		//---------------------------------------------------------------------

		if (ja.contains("''")) {
			int jqc = (" " + ja + " ").split("'").length - 1;
			int eqc = (" " + en + " ").split("'").length - 1;
			if (jqc % 2 == 1 && jqc != eqc) {
				vc.error("シングル・クォートの数を確認してください。");
			}
		}

		if (ja.contains("\"\"")) {
			int jqc = (" " + ja + " ").split("\"").length - 1;
			int eqc = (" " + en + " ").split("\"").length - 1;
			if (jqc % 2 == 1 && jqc != eqc) {
				vc.error("ダブル・クォートの数を確認してください。");
			}
		}

		//---------------------------------------------------------------------
		// 用語チェック
		//---------------------------------------------------------------------

		for (Property vTerm : vTermProp) {

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

			if (ja.matches("(?s).*?" + ng + ".*")) {
				if (ok.length() == 0) {
					vc.error("「" + ng + "」は使用禁止です。");
				} else {
					vc.error("「" + ng + "」は「" + ok + "」である必要があります。");
				}
			}
		}

		//---------------------------------------------------------------------
		// 対訳正規表現チェック
		//---------------------------------------------------------------------

		Property iProp = getIllegalTranslationProperty(en, ja);
		if (iProp != null) {
			vc.error("「" + iProp.key + "」は「" + iProp.value + "」と対応している必要があります。");
		}

		//---------------------------------------------------------------------
		// & を含むパラメーター付き URL が含まれていないか判定 (ニーモニック不正対応)
		//---------------------------------------------------------------------
		// 現在、URL の & はニーモニックとして扱っていないため検証不要 2012.09.23
		//if (Mnemonics.containsUrlParameters(en)) {
		//	vc.error("& を含むパラメーター付き URL が含まれてるエントリーは追加できません。");
		//}

		//---------------------------------------------------------------------
		// ). など記号のみのエントリーをチェック
		//---------------------------------------------------------------------
		if (en.matches("\\p{Punct}+")) {
			vc.error("原文が記号のみで構成されています。不正エントリー?");
		}

		//---------------------------------------------------------------------
		// 翻訳スルー正規表現プロパティー・チェック
		//---------------------------------------------------------------------
		for (String t : regexThroughSet) {
			if (en.matches(t)) {
				vc.error("翻訳スルー正規表現プロパティーに定義されています。" + t);
			}
		}

		return vc;
	}

	/**
	 * 末尾文字を検証します。
	 * <p>
	 * @param en 英語
	 * @param ja 日本語
	 * @param vc 検証コンテキスト
	 */
	protected void validateSuffix(String en, String ja, ValidationContext vc) {

		// 「...」チェック (末尾以外でも OK)
		if (en.contains("...") && !ja.contains("...")) {
			// 訳文にその他複数を示す「、、、」、範囲を示す「から」があれば OK
			if (!ja.contains("、、、") && !ja.contains("から")) {
				vc.error("原文に「...」が含まれる場合は、訳文にも「...」が必要です。");
			}
		}

		// 末尾の「..」チェック
		if (!en.contains("...") && en.endsWith("..") && !ja.endsWith("..")) {
			vc.error("原文の末尾に「..」がある場合は、訳文の末尾にも「..」が必要です。");
		}

		// 末尾の「.」チェック
		else if (!en.endsWith("..") && en.endsWith(".") && !ja.endsWith("。")) {

			// ・1 単語の場合はチェックしない
			// ・途中に「:」が含まれる場合はチェックしない
			// 例）Incompatible\ file\ format.\ Workspace\ was\ saved\ with\ an\ incompatible\ version\:\ {0}.
			if (en.contains(" ") && !en.contains(":") && !TranslationString.endsWithAbbreviations(en)) {
				vc.warn("原文末尾にある「.」が句点を表す場合は、訳文の末尾にも「。」が必要です。");
			}
		}

		// 末尾の「.」か「;」なし、訳文末尾「。」ありチェック
		// -> エラーとなる数が多いためチェックしないが、TranslationRule で最適化は行われる
		//else if (!en.endsWith(".") && !en.endsWith(";") && ja.endsWith("。")) {
		//	vc.error("原文の末尾に「.」か「;」がない場合は、訳文の末尾に「。」は不要です。");
		//}

		// 末尾の「:」チェック
		else if (en.endsWith(":") && !ja.endsWith(":")) {
			vc.error("原文の末尾に「:」がある場合は、訳文の末尾にも「:」が必要です。");
		}
	}
}
