/*
 * 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.resource;

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jp.sourceforge.mergedoc.org.apache.commons.lang.StringUtils;
import jp.sourceforge.mergedoc.pleiades.log.Logger;

/**
 * 翻訳対象となる項目を表すクラスです。
 * 冗長な翻訳エントリーに対応するために、トリム・復元、
 * 句点解析によるエントリーの分割を行うことができます。
 * <p>
 * @author cypher256
 */
public class TranslationString {

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

	/** トリムする空白文字の配列 (\u00A0 は nbsp 制御文字) */
	private static final char[] TRIM_SPACE_CHAR_ARRAY = { ' ', '\t', '\r', '\n', '\u00A0' };

	// クラス初期化
	static {
		// バイナリー・サーチのためにトリムする空白文字の配列をソート
		Arrays.sort(TRIM_SPACE_CHAR_ARRAY);
	}

	/**
	 * new TranslationString(value).trim() のショートカットです。
	 * @param value 値
	 * @return トリム後の文字列
	 */
	public static String trim(String value) {
		return new TranslationString(value).trim();
	}

	/**
	 * 複数形を示す可能性が高い (s) を削除します。
	 * @param en 英語文字列
	 * @return 除去後の文字列、無い場合は引数の文字列。
	 */
	public static String removeS(String en) {

		String result = removeSorNull(en);
		return result == null ? en : result;
	}

	/**
	 * 複数形を示す可能性が高い (s) を削除します。
	 * @param en 英語文字列
	 * @return 除去後の文字列、無い場合は null。
	 */
	public static String removeSorNull(String en) {

		if (en.contains("(s)") && !en.contains(" (s)")) {
			return en.replace("(s)", "");
		}
		return null;
	}

	/**
	 * 末尾が省略後か判定します。
	 * @param en 英語
	 * @return 末尾が省略後の場合は true
	 */
	public static boolean endsWithAbbrev(String en) {

		return en.matches("(?si).*?[\\s\\p{Punct}](Inc|Ltd|etc|sec|max|min|Reg|Exp|Misc|e\\.g|i\\.e)\\.");
	}

	// -------------------------------------------------------------------------

	/** 元の文字列 */
	private final String original;

	/** 先頭部 */
	protected StringBuilder starts;

	/** 末尾部 */
	protected StringBuilder ends;

	/** 前後空白を除いた文字列 */
	private String body;

	/**
	 * 翻訳項目を構築します。
	 * @param value 文字列
	 */
	public TranslationString(String value) {

		original = value;
		init();
	}

	/**
	 * 初期化します。
	 * trim 時に退避された文字列はクリアされます。
	 */
	protected void init() {

		starts = new StringBuilder();
		ends = new StringBuilder();
		body = null;
	}

	/**
	 * このオブジェクトの文字列表現を返します。
	 * コンストラクタでは渡された文字列参照を単純に返すだけで、何も処理は行われません。
	 * <p>
	 * @return コンストラクタでは渡された文字列参照
	 */
	@Override
	public String toString() {
		return original;
	}

	/**
	 * 翻訳用にトリムします。
	 * <p>
	 * 翻訳用トリムとは翻訳に影響を与えない前後文字列を除去することを指します。
	 * 除去された文字列は revert メソッドで復元することができます。
	 * トリムの対象を以下に示します。
	 * <pre>
	 * ・前後の \n、\r、\t、半角空白
	 * ・前後の囲み文字 ()、[]、<>、""、''、!! など (前後の組み合わせが一致する場合)
	 * ・先頭の IWAB0001E のような Eclipse 固有メッセージ・コード
	 * ・複数形を示す (s) (秒を示すものではない場合)
	 * <pre>
	 * 辞書プロパティーに格納されているすべてのエントリーはこのメソッドでトリムされています。
	 * <p>
	 * <p>
	 * 注意：trimForce 後に trim すると、最初の trimForce 後に対して trim されます。
	 * リセットされるわけではありません。
	 * <p>
	 * @return 文字列
	 */
	public String trim() {

		if (body != null) {
			return body;
		}
		body = original;
		trimInternal();
		return body;
	}

	/**
	 * 一連のトリム処理を行います。
	 */
	private void trimInternal() {

		// 前後空白除去
		trimSpace();

		// 前後囲み文字の除去
		trimEnclosed("(", ")");
		trimEnclosed("[", "]");
		trimEnclosed("<!--", "-->");
		trimEnclosed("<a>", "</a>");
		trimEnclosed("<", ">");
		trimEnclosed("「", "」");

		trimEnclosed("\"");
		trimEnclosed("'");
		trimEnclosed("!");

		// HTML 特定文字の除去 (HTML 処理より前に行う)
		trimSpecific("&nbsp;");

		// HTML の場合の処理
		if (body.startsWith("<") || body.endsWith(">")) {

			final String BLOCK_TAG_NAMES =
					"br|hr|div|p|ul|ol|dl|li|dd|h[1-5]|table|t[rdh]|html|body|!-- .+ --)(\\s+[^<>]+?|/?)";

			// HTML 事前整形 - 空白や改行をまとめる (現状 pre タグ未考慮、table|hr は Velocity テンプレートなど)
			if (body.matches("(?si)<(!DOCTYPE|html|table|hr)(.+)</[a-z]+>")) {

				// ブロックタグやコメントタグ前後の空白は除去
				body = body.replaceAll("(?si)\\s*(</?(" + BLOCK_TAG_NAMES + ">)\\s*", "$1");

				// ブロックタブに隣接するインラインタグの内側の空白を除去
				body = body.replaceAll("(?si)\\s*((</[^<>]+>|)</?(" + BLOCK_TAG_NAMES + ">(<[^/<>]+>|))\\s*", "$1");

				// 空白をまとめる
				body = body.replaceAll("\\s+", " ");
			}

			// ブロックタグを除去 (HTML の場合は上記でタグ前後空白を除去済み)
			// 片側だけも対象、ただしタグが空白と隣接していないこと： 例えば "Wrap in <br>" は文のためタグを除去しない
			// 閉じタグの / 無しなど不正タグも対象 : <html>aaa<html>

			final String BLOCK = "(" +
				// ブロック開始終了タグ
				"(</?(img|" + BLOCK_TAG_NAMES + ">|" +
				// head タグ (style タグなどの中身を含めて除去)
				"<head>.*</head>|" +
				"<style>.*</style>" +
				")*)";

			Pattern blockPat = PatternCache.get("(?si)^" + BLOCK + "(\\S.*?\\S)" + BLOCK + "$");
			Matcher blockMat = blockPat.matcher(body);
			if (blockMat.find()) {
				starts.append(blockMat.group(1));
				body = blockMat.group(5);
				ends.insert(0, blockMat.group(6));
			}

			// 残ったタグを除去 (閉じタグは / 開始)
			Pattern inlinePat = PatternCache.get("(?si)^(<[^<>]+>|)" + "(\\S.*?\\S)" + "(</[^<>]+>|)$");
			Matcher inlineMat = inlinePat.matcher(body);
			while (inlineMat.find()) {

				String sTag = inlineMat.group(1);
				String eTag = inlineMat.group(3);
				if (sTag.isEmpty() && eTag.isEmpty()) {
					break;
				}

				String sTagName = "";
				if (!sTag.isEmpty()) {
					sTagName = sTag.replaceFirst("(?s)<(\\w+).*", "$1").toLowerCase();
				}
				String eTagName = "";
				if (!eTag.isEmpty()) {
					eTagName = eTag.replaceFirst("(?s)\\s*</(.+?)>", "$1").toLowerCase();
				}
				String inside = inlineMat.group(2);

				// 両サイドにタグがある場合
				if (!sTagName.isEmpty() && !eTagName.isEmpty()) {

					// 開始、終了のタグ名が異なる場合は break
					if (!sTagName.equals(eTagName)) {
						break;
					}
					// コンテンツに同じタグがある場合は除去しない (<b>x</b><b>y</b> が x</b><b>y にならないようにする)
					if (inside.contains(eTag)) { // 開始タグは属性があるため終了タグで判定
						break;
					}
				}
				// タグが片側のみの場合
				else {

					// 片側タグでコンテンツに対応するタグが含まれている場合 - 例： aaa<a>bbb</a>
					if (sTagName.isEmpty() && inside.contains("<" + eTagName)) {
						break;
					}
					if (eTagName.isEmpty() && inside.contains("</" + sTagName)) {
						break;
					}
				}

				starts.append(sTag);
				body = inside;
				ends.insert(0, eTag);
				inlineMat = inlinePat.matcher(body);
			}
		}

		// 分割後の補正 例) (App Engine - 1.2.0)
		// (App Engine
		// 1.2.0)
		if (body.startsWith("(") && !body.startsWith("( ") && !body.contains(")")) {
			trimStarts("(");
		}
		else if (
			body.endsWith(")") && !body.endsWith(" )") &&	//「Missing right parenthesis )=右括弧 ) が欠落」除外
			!body.contains("(") && !body.endsWith(":)")		// 顔文字除外
		) {
			trimEnds(")");
		}
		// 分割後の末尾 br + ハイフン
		else if (body.endsWith(">-")) {

			Pattern pat = PatternCache.get("(?si)^(.+?)(<br/?>-)$");
			Matcher mat = pat.matcher(body);
			if (mat.find()) {
				body = mat.group(1);
				ends.insert(0, mat.group(2));
			}
		}

		// 前後の連続するハイフン + 空白 例) --- hoge ---
		trimHyphen();

		// Eclipse 固有エラーコードプレフィックス 例) IWAB0001E
		if (body.startsWith("I") || body.startsWith("C")) {
			Pattern pat = PatternCache.get("(?s)^([A-Z]{3,4}\\d{4}[A-Z]{0,1}[\\s:]+)(.+)$");
			Matcher mat = pat.matcher(body);
			if (mat.find()) {
				starts.append(mat.group(1));
				body = mat.group(2);
			}
		}

		// IDEA メニュー区切り
		if (body.endsWith("//")) {
			Pattern pat = PatternCache.get("(?si)^(.+[^:/\\s])(//)$");
			Matcher mat = pat.matcher(body);
			if (mat.find()) {
				body = mat.group(1);
				ends.insert(0, mat.group(2));
			}
		}

		// IDEA 固有のサフィックス
		final String LOC = " #loc";
		if (body.endsWith(LOC)) {
			body = body.substring(0, body.length() - LOC.length());
			ends.insert(0, LOC);
		}
	}

	/**
	 * 強制トリムします。
	 * <p>
	 * 除去された文字列は revert のためにこのオブジェクト内に保持されます。
	 * 除去の対象を以下に示します。
	 * <pre>
	 * ・前後の ...、.、: など
	 * <pre>
	 * 末尾の . が 1 つの場合は 。に変換され、revert 用に保持されます。
	 * <p>
	 *
	 * 以前は以下でしたが、現在はリソース節減のため Generator の保管直前に適用されています。
	 * ───────────────────────────────────────────
	 * このメソッドの結果は翻訳に影響を与える可能性があるため辞書プロパティー生成では使用されません。
	 * 用途としては辞書参照時に訳が見つからない場合に、このメソッドで処理した後、
	 * 再検索するために使用することが想定されています。
	 *
	 * @return 文字列
	 */
	public String trimForce() {

		// 通常トリム (トリム済みの場合は処理されない)
		trim();

		// トリムできなくなるまで繰り返し (実行時と辞書作成時の差を無くすため)
		String bodyBefore = body;
		while (true) {

			// 前後タグ (後ろ閉じタグでなくも許容、不正タグも含む、ただし本文にタグがある場合は対象外)
			if (body.startsWith("<") || body.endsWith(">")) {
				final String TAG = "(</?\\w+/?>|<\\w\\s+\\w+=[^<>]+?>)*";
				Pattern pat = PatternCache.get("(?s)^(" + TAG + "\\s*)" + "([^<>]+?)" + "(\\s*" + TAG + ")$");
				Matcher mat = pat.matcher(body);
				if (mat.find()) {
					starts.append(mat.group(1));
					body = mat.group(3);
					ends.insert(0, mat.group(4));
				}
			}

			// ピリオド + 連続する英小文字の場合は何もしない 例) .project
			if (body.startsWith(".") && body.matches("\\.[a-z]+")) {
				return body;
			}

			// 句読点など指定文字でトリム
			trimSpecific(".", "。", ":", "…");
			trimEnds("?"); // ?s などの正規表現を避けるため末尾のみ

			// ! は空白が隣接しない場合のみトリム (with !)
			if (body.length() >= 2) {
				if (body.charAt(0) == '!' && body.charAt(1) != ' ') {
					trimStarts("!");
				}
				if (body.length() >= 2 &&
						body.charAt(body.length() - 1) == '!' && body.charAt(body.length() - 2) != ' ') {
					trimEnds("!");
				}
			}

			// もう一度通常トリム
			trimInternal();

			if (bodyBefore.equals(body)) {
				break;
			}
			bodyBefore = body;
		}

		// 除去した ends の . が 1 つの場合は 。に変換
		// body が 2 桁以下の場合は除外 (1. xxx や &1 など)
		// min. などの省略形は min.=秒 のエントリーを作成して、このメソッドが呼ばれないようにしておく必要あり
		String e = ends.toString();
		if (e.contains(".") && !e.contains("..") && body.length() > 2) {
			int pos = e.indexOf(".");
			int posNext = pos + 1;

			// . の次が英字の場合は除外、拡張子のドットなど
			if (posNext < ends.length() && Character.isLetter(ends.charAt(posNext))) {
			}
			// aaa.bbb.ccc. のようなパッケージ形式の場合は除外
			else if (!body.contains(" ") && body.contains(".")) {
			}
			else {
				ends.replace(pos, pos + 1, "。");
			}
		}
		return body;
	}

	/**
	 * 空白を除去します。
	 */
	private void trimSpace() {

		// パフォーマンスを考慮し可能な限り正規表現は使用しない

		if (body.trim().isEmpty()) {
			starts.append(body);
			body = "";
			return;
		}

		char[] cs = body.toCharArray();

		// 先頭部トリム
		int sCount = 0;
		for (int i = 0; i < cs.length; i++) {
			char c = cs[i];
			if (Arrays.binarySearch(TRIM_SPACE_CHAR_ARRAY, c) >= 0) {
				starts.append(c);
				sCount++;
			} else {
				break;
			}
		}

		// 後方部トリム
		int eCount = 0;
		for (int i = cs.length - 1; i > 0; i--) {
			char c = cs[i];
			if (Arrays.binarySearch(TRIM_SPACE_CHAR_ARRAY, c) >= 0) {
				ends.insert(0, c);
				eCount++;
			} else {
				break;
			}
		}

		// トリム後の文字列
		body = body.substring(sCount, body.length() - eCount);
	}

	/**
	 * 囲み文字列を除去します。<br>
	 * trimEnclosed(s, s); と同じです。
	 * <p>
	 * @param s 囲み文字列
	 */
	private void trimEnclosed(String s) {
		trimEnclosed(s, s);
	}

	/**
	 * 指定された囲み文字列を除去します。
	 * 囲み文字列の外側に句読点 。や . がある場合はそれも対象となり、
	 * 除去された文字列は revert のためにこのオブジェクト内に保持されます。
	 * その時、. は 。に変換されます。
	 * <p>
	 * @param s 開始文字列
	 * @param e 終了文字列
	 */
	private void trimEnclosed(String s, String e) {

		if (!body.startsWith(s)) {
			return;
		}
		if (StringUtils.countMatches(body, s) != (s.equals(e) ? 2 : 1)) {
			return;
		}
		if (body.endsWith(".") || body.endsWith("。")) {
			e += body.substring(body.length() - 1);
		}
		if (!body.endsWith(e)) {
			return;
		}

		int sLen = s.length();
		int eLen = e.length();
		int bLen = body.length();

		starts.append(body.substring(0, sLen));
		ends.insert(0, body.substring(bLen - eLen).replace('.', '。'));
		body = body.substring(sLen, bLen - eLen);

		trimSpace();
	}

	/**
	 * 前後の連続する特定の文字をトリムします。
	 * @param ss トリムする文字列の配列
	 */
	private void trimSpecific(String... ss) {

		trimStarts(ss);
		trimEnds(ss);
	}

	/**
	 * 先頭の連続する特定の文字をトリムします。
	 * @param ss 文字列の配列
	 */
	private void trimStarts(String... ss) {

		while (true) {
			int len = body.length();
			for (String s : ss) {
				if (body.startsWith(s)) {
					starts.append(s);
					body = body.substring(s.length());
					trimSpace();
				}
			}
			if (body.length() == len || body.isEmpty()) {
				break;
			}
		}
	}

	/**
	 * 末尾の連続する特定の文字をトリムします。
	 * @param ss 文字列の配列
	 */
	private void trimEnds(String... ss) {

		while (true) {
			int len = body.length();
			for (String s : ss) {
				if (body.endsWith(s)) {
					ends.insert(0, s);
					body = body.substring(0, body.length() - s.length());
					trimSpace();
				}
			}
			if (body.length() == len || body.isEmpty()) {
				break;
			}
		}
	}

	/**
	 * 前後の連続する文字 + 空白を除去します。
	 */
	protected void trimHyphen() {

		if (trimStartsWithSpace("-")) {
			trimEndsWithSpace("-");
		}
	}

	/**
	 * 先頭の連続する文字 + 空白を除去します。
	 * @param s 文字
	 * @return 除去した場合は true
	 */
	protected boolean trimStartsWithSpace(String s) {

		if (body.startsWith(s)) {
			Pattern pat = PatternCache.get("(?s)^(" + s + "+\\s+)(.+)$");
			Matcher mat = pat.matcher(body);
			if (mat.find()) {
				starts.append(mat.group(1));
				body = mat.group(2);
				return true;
			}
		}
		return false;
	}

	/**
	 * 末尾の空白 + 連続する文字を除去します。
	 * @param s 文字
	 * @return 除去した場合は true
	 */
	protected boolean trimEndsWithSpace(String s) {

		if (body.endsWith(s)) {
			Pattern pat = PatternCache.get("(?s)^(.+?)(\\s+" + s + "+)$");
			Matcher mat = pat.matcher(body);
			if (mat.find()) {
				body = mat.group(1);
				ends.insert(0, mat.group(2));
				return true;
			}
		}
		return false;
	}

	/**
	 * trim により除去された文字列を復元します。
	 * このメソッドでは toString() と異なり trim された末尾の . は 。として復元されます。
	 * <p>
	 * @return 連結後の文字列
	 */
	public String revert() {
		trim();
		return starts + body + ends;
	}

	/**
	 * trim により除去された文字列を指定された文字列の前後に復元します。
	 * trim された末尾の . は 。として復元されます。
	 * <p>
	 * @param jaBody 新しい文字列 (日本語)
	 * @return 連結後の文字列
	 */
	public String revert(String jaBody) {
		trim();
		return starts + jaBody + ends;
	}

	/**
	 * 分割時の末尾要素以外の翻訳文字列クラスです。
	 */
	private static class SplitTranslationString extends TranslationString {

		public SplitTranslationString(String value) {
			super(value);
		}

		@Override
		protected void trimHyphen() {

			// 末尾の - が句点分割で残っているため。
			// 親クラスの動作では先頭に - がないと末尾 - は除去されない。
			trimEndsWithSpace("-");
		}

		@Override
		public String revert(String jaBody) {

			trim();

			// 分割された翻訳文字列の特殊句点処理。英語ピリオド後の空白除去
			// 英語「. 」 -> 日本語「。」
			// 英語「.&nbsp;」 -> 日本語「。」
			if (jaBody.endsWith("。") && ends.length() != 0) {
				if (ends.charAt(0) == ' ') {
					ends.deleteCharAt(0);
				} else if (ends.toString().startsWith("&nbsp;")) {
					ends.delete(0, 6);
				}
			}
			// 強制トリムされていた場合
			else if (ends.toString().startsWith("。 ")) {
				ends.deleteCharAt(1);
			}
			return starts + jaBody + ends;
		}
	}

	/**
	 * 文字列を「。」や「. 」などの句点で分割した翻訳文字列のリストを取得します。
	 * 分割された翻訳文字列には末尾の句点が含まれます。
	 * 分割できない場合、意図しない二重 trim や二重 revert を避けるため null を返します。
	 * つまり、戻り値は必ずサイズ 2 以上のリストまたは null になります。
	 * <p>
	 * リストの先頭要素の先頭と、末尾要素の末尾はトリムされます。
	 * 復元するには、リストをすべて連結し、revert することで復元できます。
	 * <p>
	 * @return 句点分割した翻訳文字列リスト (分割できない場合は null)
	 */
	public List<TranslationString> split() {
		return split(null);
	}

	/**
	 * 文字列を「。」や「. 」などの句点で分割した翻訳文字列のリストを取得します。
	 * 分割された翻訳文字列には末尾の句点が含まれます。
	 * 分割できない場合、意図しない二重 trim や二重 revert を避けるため null を返します。
	 * つまり、戻り値は必ずサイズ 2 以上のリストまたは null になります。
	 * <p>
	 * リストの先頭要素の先頭と、末尾要素の末尾はトリムされます。
	 * 復元するには、リストをすべて連結し、revert することで復元できます。
	 * <p>
	 * @param additionalPattern 追加分割正規表現パターン (si 適用あり、デフォルト null)
	 * @return 句点分割した翻訳文字列リスト (分割できない場合は null)
	 */
	public List<TranslationString> split(String additionalPattern) {

		// trimForce が呼ばれていたときのために元に戻してから trim
		init();
		trim();

		// 整形済みテキストや文中に改行がある場合は分割しない
		// body タグがある場合の改行は無視
		// 注意：分割判定に使用されるため、対応する日本語でも同じ判定結果となる必要あり
		if (additionalPattern == null && original.contains("\n") && !original.matches("(?is).*<(html|body|table).*")) {
			if (
				original.matches("(?s).*?\\n+[ \t].*") ||		// 改行 + 空白 ⇒ 整形済みテキスト
				original.matches("(?s).*?[^\\.!。\\n]\\n.+")	// ドット以外 + 改行 ⇒ 文中の改行
			) {
				return null;
			}
		}

		List<String> list = new ArrayList<String>();

		// 先頭、末尾の () や [] を分割
		for (String s : splitParenthesis(body)) {

			// 句点分割
			list.addAll(splitPunct(s, additionalPattern));
		}

		// 分割 1 の場合は二重トリムや二重 revert を避けるため null を返す
		if (list.size() == 1) {
			return null;
		}

		// TranslationString のリストに変換
		List<TranslationString> tsList = new ArrayList<TranslationString>();
		for (int i = 0; i < list.size(); i++) {
			if (i < list.size() - 1) {
				// 最後の要素以外は特殊処理を行うため拡張クラス
				tsList.add(new SplitTranslationString(list.get(i)));
			} else {
				// 最後の要素
				tsList.add(new TranslationString(list.get(i)));
			}
		}
		return tsList;
	}

	/**
	 * 先頭、末尾の括弧 (.+) [.+] を分割します。
	 * ただし、下記の場合を除きます。
	 * <pre>
	 * ・[{0}] のように { が含まれる (訳文の一部になる可能性があるため)
	 * </pre>
	 * @param value 文字列
	 * @return 分割後のリスト。分割できない場合はサイズ 1。
	 */
	private List<String> splitParenthesis(String value) {

		List<String> list = new ArrayList<String>();
		if (!value.contains("(") && !value.contains("[")) {
			list.add(value);
			return list;
		}

		// 特定のタグがある場合は分割しない (IDEA tips のショートカットなどは文の一部)
		if (value.contains("<span")) {
			list.add(value);
			return list;
		}

		final String PARENTH = "[\\(\\[][^\\{]+[\\)\\]]";
		final String SPACE_GROUP = "([\\s\u00A0]|&nbsp;)"; // \u00A0 は nbsp 制御文字

		// 括弧が先頭にあるパターン
		Pattern pat = PatternCache.get("(?s)^" + "(" + PARENTH + SPACE_GROUP + ")" + "(.+)$");
		Matcher mat = pat.matcher(value);
		boolean isFound = false;
		if (mat.find()) {
			isFound = true;
		}
		// 括弧が末尾にあるパターン
		else {
			final String ENDS_PARENTH_GROUP = "(" + PARENTH + "[\\.。:]?\\s*)$";
			pat = PatternCache.get("(?s)^" + "(.+" + SPACE_GROUP + ")" + ENDS_PARENTH_GROUP);
			mat = pat.matcher(value);
			if (mat.find()) {

				// "(xxx)yyy)" みたいになった場合、g1 を最短一致に切り替え
				if (invalidParenthesis(mat.group(2))) {

					pat = PatternCache.get("(?s)^" + "(.+?" + SPACE_GROUP + ")" + ENDS_PARENTH_GROUP);
					mat = pat.matcher(value);
					isFound = mat.find();
				} else {
					isFound = true;
				}
			}
		}

		if (isFound) {

			String g1 = mat.group(1);
			String g2 = mat.group(3);

			if (invalidParenthesis(g1) || invalidParenthesis(g2)) {
				// 括弧の対応が不正
				list.add(value);
			} else {
				// 再帰
				list.addAll(splitParenthesis(g1));
				list.addAll(splitParenthesis(g2));
			}
		} else {
			// 分割なし
			list.add(value);
		}
		return list;
	}

	/**
	 * 括弧の対応が不正か判定します。
	 * @param value 文字列
	 * @return 不正な場合は true
	 */
	private boolean invalidParenthesis(String value) {
		return
			StringUtils.countMatches(value, "(") != StringUtils.countMatches(value, ")") ||
			StringUtils.countMatches(value, "[") != StringUtils.countMatches(value, "]");
	}

	/**
	 * 句点分割します。
	 * <p>
	 * @param value 文字列
	 * @param additionalPattern 追加分割正規表現パターン (si 適用あり、デフォルト null)
	 * @return 分割後のリスト
	 */
	private List<String> splitPunct(String value, String additionalPattern) {

		List<String> list = new ArrayList<String>();

		// 句点分割しない接頭辞
		if (value.startsWith("...")) {
			list.add(value);
			return list;
		}

		// 句読点に続く BR のパターン
		final String PUNCT_BR_PATTERN =
				// br + 連続するタグ
				"\\s*<br/?>(<[^<>]+>)*|" +
				// /b + 連続する br
				"</b>(<br>)*\\s+|" +
				// 空白参照
				"&nbsp;";

		// 追加分割パターン
		String additional = "";
		if (additionalPattern != null && !additionalPattern.isEmpty()) {
			additional = additionalPattern + "|";
		}

		final String BLOCK_START = "(ul|ol|dl|su[pb]|div|p)(\\s+[^>]+|)";

		// 分割パターン
		Pattern pat = PatternCache.get("(?si)\\s*(" +
				additional +
				// 日本語の句点	+ br、/b+空白 (英語と異なり後続の空白は無くてもよい)
				"[。：？！]+("		+ PUNCT_BR_PATTERN + "|)+|" +
				// 英語の句点	+ br、/b+空白、空白
				"[\\.:;\\?!]+("		+ PUNCT_BR_PATTERN + "|\\s)+|" +
				// ブロック終了と開始タグ連続 (開始タグはあとで次の要素先頭へ移動)
				"(<(/p|p/|/li|/h[1-5]|/td|/tr|/table|" + BLOCK_START + ")>)+|" +
				// /b + br タグ
				"</b>(<br/?>)+|" +
				// br タグ 2 つ以上
				"(<br/?>){2,}(<[^<>]+>)*|" +
				// br + ハイフン + 空白
				"<br/?>-\\s|" +
				// ハイフン (前後空白、ただし trim や trimForce では除去できない) 例 "char is -"
				"\\s-\\s|" +
				// コメントタグ (空白が隣接していないか処理内でチェック)
				"<!-- .+ -->|" +
				// スラッシュ 2 つ 、URL で使用される :// や /// は除外 (IDEA メニュー区切り)
				"[^:/<>\\s]//" +
				")\\s*");

		Matcher mat = pat.matcher(value);

		try {
			StringBuffer sb = new StringBuffer();
			int prevEndPos = 0;

			while (mat.find()) {

				String sep = mat.group();
				int sPos = mat.start();
				int ePos = mat.end();

				if (sPos > 0 && ePos < value.length()) {

					// 区切りより前の文字列
					String s = value.substring(prevEndPos, sPos);

					// 区切りより後の文字列
					String e = value.substring(ePos);

					// [:-] の後に {n} があり、その後に句読点以外がある、つまり文中で意味を成す場合は分割しない
					//
					// 例)	The non default location: {0} for {1} is in use.
					//		→ {1} 用の非デフォルト・ロケーション\: {0} は使用中です
					//
					//		No data source available for data set - {0}, please select a data source
					//		→ データ・セット {0} にデータ・ソースがありません。～
					if ((sep.contains(":") || sep.contains("-"))) {

						// 次のパートを取得して比較
						Matcher nextMat = pat.matcher(e);
						if (nextMat.find()) {
							e = e.substring(0, nextMat.start());
						}
						if (e.contains("{") && e.matches("(?s).*?[^\\p{Punct}\\d].*")) {
							continue;
						}
					}

					// : が () で囲まれている場合は分割しない
					if (sep.contains(":")) {
						// 例) (EJB 2.0\: 22.2、22.5)
						if (s.contains("(") && e.contains(")")) {
							continue;
						}
						// 例) {0} [Runner: {1}]
						if (s.contains("[") && e.contains("]")) {
							continue;
						}
						// 例）style="border-collapse: collapse"
						if (s.contains(" style=\"") && e.contains("\"")) {
							continue;
						}
					}
					// 前が _ の場合 (文字そのものを表す場合)
					// 例) A-Z a-z 0-9 . _ -
					// ハイフンの前後が数値の場合は分割しない
					// 例) 1 - 2
					else if (sep.contains("-")) {
						if (s.endsWith("_")) {
							continue;
						}
						if (s.matches("(?s).*\\d") && e.matches("(?s)\\d.*")) {
							continue;
						}
					}
					// ... の前後に空白
					// 例) INSERT ... VALUES (...)、 (...)、 ...、
					// 前が空白の場合 (文字そのものを表す場合)
					// 例) A-Z a-z 0-9 . _ -
					// . が省略を意味する場合は分割しない
					// 例) hoge Inc. xxx
					else if (sep.contains(".")) {

						// 前に空白が無くても ...  は分割しない 2017.03.24
						// 例) Settings Repository... to configure
						//if (sep.contains(" ... ")) {
						if (sep.contains("... ")) {
							continue;
						}
						if (sep.contains(" .")) {
							continue;
						}
						if (endsWithAbbrev(s + sep.charAt(0))) {
							continue;
						}
					}
					// ; が HTML 参照文字の末尾の場合は分割しない
					// 例) &lt;name&gt; is a file
					else if (sep.startsWith(";")) {
						if (s.contains("&") && s.matches("(?si).*&[a-z]+")) {
							continue;
						}
					}
					// 。の次が ) の場合は分割しない
					// 例) 。)
					else if (sep.endsWith("。")) {
						if (e.startsWith(")")) {
							continue;
						}
					}
					// ? の次が = の場合は分割しない
					// 例) (* = any string, ? = any character)
					else if (sep.contains("?")) {
						if (e.startsWith("=")) {
							continue;
						}
					}
					//
					// タグ前後に空白 (タブや改行除く)
					// 例）Discard empty <p> elements
					else if (sep.contains(" <") && sep.contains("> ") && sep.matches(" +<.+?> +")) {
						continue;
					}
					// コメントタグの前後に空白がある場合は分割しない (HTML の場合、空白は事前に除去されている)
					else if (sep.contains("<!-- ") && sep.contains(" -->")) {
						if (sep.contains(" <!-- ") || sep.contains(" --> ")) {
							continue;
						}
					}
				}

				mat.appendReplacement(sb, Matcher.quoteReplacement(sep));
				list.add(sb.toString());
				sb.delete(0, sb.length());
				prevEndPos = ePos;
			}

			mat.appendTail(sb);
			if (sb.length() > 0) {
				list.add(sb.toString());
			}

			// 要素ごとの末尾のブロック開始タグを次の要素の先頭につける
			Pattern endPat = PatternCache.get("(?si)^(.*?)((<(b|font[^>]*|" + BLOCK_START + ")>)+\\s*)$");
			for (int i = 0; i < list.size() - 1; i++) {
				String s = list.get(i);
				if (s.contains("<")) {
					Matcher endMat = endPat.matcher(s);
					if (endMat.find()) {
						list.set(i, endMat.group(1));
						list.set(i + 1, endMat.group(2) + list.get(i + 1));
					}
				}
			}

			// 句点分割した結果を括弧分割
			/*	今のところ不要、採用する場合は既存への影響を検証
			List<String> newList = new ArrayList<String>();
			for (String s : list) {
				newList.addAll(splitParenthesis(s));
			}
			list = newList;
			*/

			list.remove("");

		} catch (RuntimeException e) {
			log.error(e, "value[" + value + "] group[" + mat.group() + "]");
			throw e;
		}

		return list;
	}

	/**
	 * 全辞書フルスキャンパフォーマンス確認 main メソッドです。
	 * @param args
	 */
	public static void main(String[] args) {

		PropertySet prop = new PropertySet(TRANS_PROP);
		long s = System.currentTimeMillis();

		for (Property p : prop) {

			TranslationProperty tp = new TranslationProperty(p);
			List<TranslationString> enList = tp.en.split();
			if (enList == null) {
				new TranslationString(p.key).trimForce();
			} else {
				for (TranslationString ts : enList) ts.trimForce();
			}

			List<TranslationString> jaList = tp.ja.split();
			if (jaList == null) {
				new TranslationString(p.value).trimForce();
			} else {
				for (TranslationString ts : jaList) ts.trimForce();
			}
		}
		log.info("%.3f ms", (System.currentTimeMillis() - s) / 1000d);
	}
}
