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

import jp.sourceforge.mergedoc.pleiades.log.Logger;

import org.apache.commons.lang.StringUtils;

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

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

	/** トリムする空白文字の配列 */
	private static final char[] spaceChars = { ' ', '\t', '\r', '\n' };

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

	/**
	 * 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) {

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

	/**
	 * フォーマット済みテキスト (改行の後に \\s がある) か判定します。
	 * 現在、このメソッドは未使用。将来、改行自動最適化を実装した場合に使用するかも。
	 * @param en 英語
	 * @return フォーマット済みの場合は true
	 */
	public static boolean isFormatedText(String en) {

		return en.contains("\n") && en.matches("(?s).*?\\n\\s.*");
	}

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

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

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

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

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

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

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

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

		original = value;
		init();
	}

	/**
	 * 初期化します。
	 * trim 時に退避された文字列はクリアされます。
	 */
	private 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>
	 * @return 文字列
	 */
	public String trim() {

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

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

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

		// 分割後の補正 例) (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(")");
		}

		// 特定文字の除去
		trimSpecific("&nbsp;");

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

		// 先頭部の Eclipse 固有エラーコード 例) IWAB0001E
		if (body.startsWith("I") || body.startsWith("C")) {
			Pattern pat = Pattern.compile("(?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);
			}
		}

		return body;
	}

	/**
	 * 空白を除去します。
	 * <p>
	 * @param str 対象文字列
	 */
	private void trimSpace(String str) {

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

		if (str.trim().length() == 0) {
			starts.append(str);
			body = "";
			return;
		}

		char[] cs = str.toCharArray();

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

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

		// トリム後の文字列
		if (starts.length() == original.length() || ends.length() == original.length()) {
			body = "";
			return;
		}
		body = original.substring(starts.length(), original.length() - ends.length());
	}

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

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

	/**
	 * 前後の連続する特定の文字をトリムします。
	 * @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(body);
				}
			}
			if (body.length() == len || body.length() == 0) {
				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(body);
				}
			}
			if (body.length() == len || body.length() == 0) {
				break;
			}
		}
	}

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

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

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

		if (body.startsWith(s)) {
			Pattern pat = Pattern.compile("(?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 = Pattern.compile("(?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;
	}

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

		// 通常トリム
		trim();

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

		// 指定文字でトリム
		trimSpecific(".", ":", "?", "!");

		// 末尾の . が 1 つの場合は 。に変換
		String e = ends.toString();
		if (e.startsWith(".") && !e.startsWith("..")) {
			ends.replace(0, 1, "。");
		}
		return body;
	}

	/**
	 * 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() {

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

		// フォーマット済みテキスト (改行の後に \\s がある) の場合は分割しない
		// if (isFormatedText(original)) { // 現在、改行の除去復元に未対応

		// 改行がある場合は分割しない
		if (original.contains("\n")) {
			return null;
		}

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

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

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

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

		// TranslationString のリストに変換
		List<TranslationString> tsList = new LinkedList<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 分割後のリスト
	 */
	private List<String> splitParenthesis(String value) {

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

		// 先頭パターン
		Pattern pat = Pattern.compile("(?s)^" + "([\\(\\[][^\\{]+[\\)\\]]\\s)" + "(.+)$");
		Matcher mat = pat.matcher(value);
		boolean isFound = false;
		if (mat.find()) {
			isFound = true;

			// 末尾パターン
		} else {
			pat = Pattern.compile("(?s)^" + "(.+\\s)" + "([\\(\\[][^\\{]+[\\)\\]](|[\\.。:])\\s*)$");
			mat = pat.matcher(value);
			if (mat.find()) {

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

					pat = Pattern.compile("(?s)^" + "(.+?\\s)" + "([\\(\\[][^\\{]+[\\)\\]](|[\\.。:])\\s*)$");
					mat = pat.matcher(value);
					isFound = mat.find();
				} else {
					isFound = true;
				}
			}
		}

		if (isFound) {

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

			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 文字列
	 * @return 分割後のリスト
	 */
	private List<String> splitPunct(String value) {

		// 分割パターン
		Pattern pat = Pattern.compile("\\s*(" + "[。：？！]+|" + // 日本語の句点
				"[\\.:;\\?!]+(\\s|&nbsp;)+|" + // 英語の句点 (次の文字が空白)
				"\\s-\\s" + // ハイフン (前後空白)
				")\\s*");
		Matcher mat = pat.matcher(value);
		List<String> list = new LinkedList<String>();

		try {
			StringBuffer sb = new StringBuffer();
			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(0, sPos);
					String e = value.substring(ePos);

					// [:-] の後に {n} があり、その後に単語、つまり文中で意味を成す場合は分割しない
					// 例) The\ non\ default\ location\:\ {0}\ for\ {1}\ is\ in\
					// use.
					// No\ data\ source\ available\ for\ data\ set\ -\ {0},\
					// please\ select\ a\ data\ source
					if (sep.contains(":") || sep.contains("-")) {
						if (e.contains("{") && e.matches("(?s).*?[^\\p{Punct}\\d].*")) {
							continue;
						}
					}
					// : が () で囲まれている場合は分割しない
					// 例) (EJB 2.0\: 22.2、22.5)
					if (sep.contains(":")) {
						if (s.contains("(") && 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(".")) {
						if (sep.equals(" ... ")) {
							continue;
						}
						if (sep.contains(" .")) {
							continue;
						}
						if (endsWithAbbreviations(s + sep.charAt(0))) {
							continue;
						}
					}
					// ; が HTML 参照文字の末尾の場合は分割しない
					// 例) &lt;name&gt; is a file
					else if (sep.startsWith(";")) {
						String ref = s.replaceFirst("(?s).*(&.+)", "$1") + sep;
						if (Mnemonics.containsHtmlReference(ref)) {
							continue;
						}
					}
					// 。の次が ) の場合は分割しない
					// 例) 。)
					else if (sep.endsWith("。")) {
						if (e.startsWith(")")) {
							continue;
						}
					}
					// ? の次が = の場合は分割しない
					// 例) (* = any string, ? = any character)
					else if (sep.contains("?")) {
						if (e.startsWith("=")) {
							continue;
						}
					}
				}
				mat.appendReplacement(sb, Matcher.quoteReplacement(sep));
				list.add(sb.toString());
				sb.delete(0, sb.length());
			}

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

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

		return list;
	}
}
