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

import java.io.File;

import jp.sourceforge.mergedoc.pleiades.Pleiades;
import jp.sourceforge.mergedoc.pleiades.PleiadesOption;
import jp.sourceforge.mergedoc.pleiades.aspect.advice.JointPoint;
import jp.sourceforge.mergedoc.pleiades.aspect.util.CachePropertySet;
import jp.sourceforge.mergedoc.pleiades.aspect.util.ValueHashPropertySet;
import jp.sourceforge.mergedoc.pleiades.log.Logger;
import jp.sourceforge.mergedoc.pleiades.resource.AbstractTranslationDictionary;
import jp.sourceforge.mergedoc.pleiades.resource.FileNames;
import jp.sourceforge.mergedoc.pleiades.resource.Mnemonics;
import jp.sourceforge.mergedoc.pleiades.resource.Property;
import jp.sourceforge.mergedoc.pleiades.resource.TranslationString;

import org.apache.commons.lang.StringUtils;

/**
 * キャッシュ機構および実行時除外機構を持つ翻訳辞書クラスです。
 * Eclipse 実行時の動的翻訳に利用されるクラスです。
 * <p>
 * @author cypher256
 */
public class DynamicTranslationDictionary extends AbstractTranslationDictionary {

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

	/** 翻訳キャッシュ・プロパティー・ファイル */
	private static final File transCacheFile = Pleiades.getResourceFile(CacheFiles.TRANS_CACHE_PROP);

	/** ニーモニック変換キャッシュ・プロパティー・ファイル */
	private static final File mnemonicCacheFile = Pleiades.getResourceFile(CacheFiles.MNEMONIC_CACHE_PROP);

	/** このクラスのシングルトン・インスタンス */
	private static DynamicTranslationDictionary instance;

	/** Pleiades オプション */
	protected static PleiadesOption pleiadesOption = Pleiades.getPleiadesOption();

	/** グローバルマルチバイト文字配列 (英語圏でも使用されるマルチバイト文字) */
	private static final char[] GLOBAL_MULTIBYTES_CHARS = "‐‘’“”¤¶".toCharArray();

	/**
	 * 翻訳辞書インスタンスを取得します。
	 * <p>
	 * @return 翻訳辞書インスタンス
	 */
	public static DynamicTranslationDictionary getInstance() {

		if (instance != null) {
			return instance;
		}
		if (log.isDebugEnabled()) {
			TraceableTranslationDictionary tracer = TraceableTranslationDictionary.getInstance();
			if (tracer != null) {
				instance = tracer;
			}
		}
		if (instance == null) {
			if (pleiadesOption.isUnderscoreMnemonic) {
				instance = new UnderscoreMnemonicDictionary();
			} else {
				instance = new DynamicTranslationDictionary();
			}
		}
		return instance;
	}

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

	/** 翻訳キャッシュ・プロパティー */
	private CachePropertySet transCacheProp;

	/** ニーモニック en to ja 変換キャッシュ・プロパティー */
	private CachePropertySet mnemonicCacheProp;

	/** 呼び出し階層エクスプローラー */
	private CallHierarchyExplorer callHierarchy = CallHierarchyExplorer.getInstance();

	/**
	 * 翻訳辞書インスタンスを構築します。
	 */
	protected DynamicTranslationDictionary() {
	}

	/**
	 * 辞書ファイルをロードします。
	 */
	@Override
	protected boolean load() {

		transCacheProp = new ValueHashPropertySet(100000, "翻訳キャッシュ・プロパティー");
		mnemonicCacheProp = new CachePropertySet(5000, "ニーモニック変換キャッシュ・プロパティー");

		if (!transCacheFile.exists()) {

			// 翻訳キャッシュが存在しない場合、翻訳辞書をロード
			super.load();
			transCacheProp.load(validateExists(FileNames.TRANS_MULTIBYTES_KEY_PROP));

		} else {

			// 翻訳キャッシュをロード
			try {
				transCacheProp.load(transCacheFile);
				mnemonicCacheProp.load(mnemonicCacheFile);

			} catch (RuntimeException e) {

				// キャッシュ破損、自己復元
				log.warn("翻訳キャッシュ %s の破損を検出。復元中... - %s", transCacheFile, e.toString());
				transCacheFile.delete();
				transCacheProp.clear();
				transCacheProp.load(validateExists(FileNames.TRANS_MULTIBYTES_KEY_PROP));

				mnemonicCacheFile.delete();
				mnemonicCacheProp.clear();
			}
		}
		return true;
	}

	/**
	 * シャットダウンします。
	 * 翻訳プロパティーはキャッシュとして永続化されます。
	 */
	public void shutdown() {

		isLoadedDefault = true;

		transCacheProp.store(transCacheFile);
		mnemonicCacheProp.store(mnemonicCacheFile);
	}

	/**
	 * 指定した英語リソース文字列から日本語リソースを探します。
	 * ニーモニックは日本用に変換されます。
	 * <p>
	 * @param enWithMnemonic 英語リソース文字列 (ニーモニック含む)
	 * @param jointPoint ジョイント・ポイント
	 * @return 日本語リソース文字列 (ニーモニック含む)。
	 */
	public String lookup(String enWithMnemonic, JointPoint jointPoint) {

		// 翻訳済みの場合は高速化のため何もしない (リソース・バンドルおよび UI で呼び出されるため)
		if (
			enWithMnemonic.length() == 0
			|| Mnemonics.hasJaMnemonic(enWithMnemonic)

			// 記号などが含まれ場合があるため全角 1 文字まで許容、指定文字セットで変換できない文字は 1 バイトになる
			// ⇒ この条件に一致するのは 1% 程度で getBytes を考慮すると逆効果のため廃止 2012.08.18
			// || enWithMnemonic.length() + 1 < Strings.getBytes(enWithMnemonic, "Windows-31J").length
		) {
			return enWithMnemonic;
		}

		// 英語ニーモニックを除去
		String en = Mnemonics.removeEnMnemonic(enWithMnemonic);

		// 翻訳プロパティーの value に合致 (翻訳済み) する場合は何もしない
		if (transCacheProp.containsValue(en.trim())) {
			return enWithMnemonic;
		}

		// 翻訳プロパティーから日本語訳を取得
		String ja = lookupInternal(en, jointPoint);

		// 翻訳不要な場合 (時間がかかるため翻訳後に翻訳がある場合にのみ判定)
		if (!en.equals(ja) && callHierarchy.isExcludeTranslation(en, jointPoint)) {

			// 翻訳不要でも Javadoc、JsDoc 生成ウィザードの Package（除外指定されている）は
			// ニーモニック変換が必要 2009.02.05
			// -----
			// ただし、ニーモニック変換が不要な場合は除外 (例: Debugの構成...)。
			// 今のところハードコード。数が多いようであればプロパティー化。 2009.02.11
			if (en.equals("Debug")) {
				return enWithMnemonic;
			}
			ja = en;
		}

		// ニーモニックを日本用に変換。この変換は訳語が見つからなかった場合も行う。
		ja = convertMnemonicEnToJa(enWithMnemonic, en, ja);

		return ja;
	}

	/**
	 * 指定した英語リソース文字列から日本語リソースを探します。
	 * ニーモニックは処理されません。
	 * <p>
	 * @param en 英語リソース文字列
	 * @param jointPoint ジョイント・ポイント
	 * @return 日本語リソース文字列。翻訳できない場合は en をそのまま返す。
	 */
	public String lookupIgnoreMnemonic(String en, JointPoint jointPoint) {

		// ほとんど呼び出されないため、高速化たのための翻訳済み判定はしない

		// 翻訳プロパティーから日本語訳を取得
		String ja = lookupInternal(en, jointPoint);

		// 翻訳不要な場合 (時間がかかるため翻訳後に翻訳がある場合にのみ判定)
		if (!en.equals(ja) && callHierarchy.isExcludeTranslation(en, jointPoint)) {
			ja = en;
		}
		return ja;
	}

	/**
	 * 指定した英語ヘルプ・リソース文字列から日本語ヘルプ・リソースを探します。
	 * <p>
	 * @param en 英語ヘルプ・リソース文字列
	 * @param jointPoint ジョイント・ポイント (現在未使用)
	 * @return 日本語ヘルプ・リソース文字列。翻訳できない場合は en をそのまま返す。
	 */
	public String lookupHelp(String en, JointPoint jointPoint) {
		return getValueForHelp(en);
	}

	/**
	 * 日本語訳を取得します。
	 * <p>
	 * @param en 英語リソース文字列（ニーモニック無し）
	 * @param jointPoint ジョイント・ポイント
	 * @return 日本語リソース文字列（ニーモニック無し）。翻訳できない場合は en をそのまま返す。
	 */
	protected String lookupInternal(String en, JointPoint jointPoint) {

		// 翻訳キャッシュ・プロパティーから取得
		String ja = transCacheProp.get(en);
		if (ja != null) {
			if (ja.length() == 0) {
				ja = en;
			}
		} else {

			// 正規表現翻訳プロパティーから取得
			// 改行マークなどの特殊記号が含まれる英文はここで解決しおかないと
			// マルチバイト判定で翻訳済みと判定されることに注意 (かっこ分割もされない)
			ja = super.getValueByRegex(new TranslationString(en));

			if (ja != null) {

				// キャッシュする
				// ⇒ 正規表現でもキャッシュするように追加 2012.08.18
				// ⇒ キャッシュしていなかったが理由不明（キャッシュサイズ考慮？）
				transCacheProp.put(en, ja);

			} else {

				// 翻訳済み (マルチ・バイトが含まれる) の場合
				if (en.length() != en.getBytes().length && !StringUtils.containsAny(en, GLOBAL_MULTIBYTES_CHARS)) {
					ja = en;

					// デフォルトの翻訳プロパティーから取得
				} else {
					if (super.load()) {
						log.debug("キャッシュにないため翻訳プロパティーをロード: %s", Property.escapeKey(en));
					}
					ja = getValue(en);
				}
				// キャッシュする
				transCacheProp.put(en, en.equals(ja) ? "" : ja);
			}
		}

		// DEBUG - pleiades-config.xml の最後 trace.pattern でデバッグ可能 (TraceableTranslationDictionary)
		//if (en.startsWith("Issues")) {
		//	String s = System.currentTimeMillis() + "「" + en + "」→「" + ja + "」";
		//	log.debug(new Exception(s), "デバッグ翻訳追跡スタックトレース jointPoint:" + jointPoint);
		//	return s;
		//}

		return ja;
	}

	/**
	 * 正規表現辞書から日本語訳を取得します。
	 * super.getValue から呼び出されますが、this.getValue で実行済みのため、
	 * 空オーバーライドしています。
	 * <p>
	 * @param enTs 英語翻訳文字列
	 * @return 日本語リソース文字列（ニーモニック無し）。翻訳できない場合は null。
	 */
	@Override
	protected String getValueByRegex(TranslationString enTs) {
		return null;
	}

	/**
	 * ニーモニックを英語用から日本用に変換します。<br>
	 * 例）&x → (&X)
	 * <p>
	 * @param enWithMnemonic	英語リソース文字列  （ニーモニック有り）
	 * @param en				英語リソース文字列  （ニーモニック無し）
	 * @param ja				日本語リソース文字列（ニーモニック無し）
	 * @return					日本語リソース文字列（ニーモニック有り）
	 */
	protected String convertMnemonicEnToJa(String enWithMnemonic, String en, String ja) {

		if (pleiadesOption.isNoMnemonic) {
			return ja;
		}
		if (enWithMnemonic.length() == en.length()) {
			return ja;
		}
		String jaWithMnemonic = mnemonicCacheProp.get(enWithMnemonic);
		if (jaWithMnemonic == null) {

			jaWithMnemonic = Mnemonics.convertMnemonicEnToJa(enWithMnemonic, en, ja);
			mnemonicCacheProp.put(enWithMnemonic, jaWithMnemonic);
		}
		return jaWithMnemonic;
	}
}
