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

import java.io.File;
import java.util.HashMap;
import java.util.Map;

import jp.sourceforge.mergedoc.org.apache.commons.lang.StringUtils;
import jp.sourceforge.mergedoc.pleiades.Pleiades;
import jp.sourceforge.mergedoc.pleiades.PleiadesOption;
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.PropertySet;
import jp.sourceforge.mergedoc.pleiades.resource.TranslationString;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.PleiadesConfig;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.RuntimeJointPoint;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.TraceConfig;
import jp.sourceforge.mergedoc.pleiades.runtime.util.CachePropertySet;
import jp.sourceforge.mergedoc.pleiades.runtime.util.ValueHashPropertySet;

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

	/** ロガー */
	private static final Logger log = Logger.getLogger(RuntimeTranslationDictionary.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 RuntimeTranslationDictionary instance;

	/** パッケージ条件キープレフィックス */
	private static final String IF_KEY_PREFIX = "%IF%";

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

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

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

		if (instance == null) {
			instance = new RuntimeTranslationDictionary();
		}
		return instance;
	}

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

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

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

	/** パッケージ条件プロパティー・マップ */
	private final Map<String, PropertySet> packageConditionMap = new HashMap<String, PropertySet>();

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

	/** トレース構成 (通常実行時は null) */
	private final TraceConfig traceConfig = PleiadesConfig.getInstance().traceConfig;

	/** キャッシュを破棄する場合は true */
	public boolean isInvalidateCache;

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

		// このクラスで先に正規表現解決するため、親では使用しない
		isResolveFirstRegex = false;
	}

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

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

		if (!transCacheFile.exists()) {

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

			// super で上書き用をロードしているが先に参照する必要があるため、キャッシュにも格納しておく
			transCacheProp.putAll(transOverrideProp);

		} else {

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

			} catch (RuntimeException e) {

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

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

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

		isLoadedDefault = true;

		mnemonicCacheProp.store(mnemonicCacheFile);

		// 安全のためキャッシュが大きくなりすぎている場合はクリア (通常は発生しない想定)
		if (transCacheProp.size() > 50 * 10000) {
			log.warn("翻訳キャッシュが大きくなりすぎているためクリアしました。" + transCacheProp.size());
			transCacheProp.clear();
		}
		transCacheProp.store(transCacheFile);

		// デバッグ辞は zip でない形式でも保存
		if (Pleiades.getPleiadesOption().enabledNotFoundLog) {
			transCacheProp.store(new File(transCacheFile.getPath().replaceFirst("\\.zip$", "")));
		}
	}

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

	/**
	 * 指定した英語リソース文字列から日本語リソースを探します。
	 * <p>
	 * @param enWithMnemonic 英語リソース文字列 (ニーモニック含む) : 1 文字以上
	 * @param jointPoint ジョイント・ポイント
	 * @param mnemonics ニーモニックを処理する場合は true
	 * @return 日本語リソース文字列 (ニーモニック含む)。
	 */
	public String lookup(String enWithMnemonic, RuntimeJointPoint jointPoint, boolean mnemonics) {

		try {
			String en = enWithMnemonic;

			// ニーモニック前処理 (除去)
			if (mnemonics) {

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

					// ja ニーモニック位置が正しいっぽい → 翻訳済みとみなす
					if (enWithMnemonic.endsWith(")") || enWithMnemonic.contains(")...")) {
						return enWithMnemonic;
					}
					// 翻訳後文字列連結によるニーモニック位置不正は再翻訳できるようにしておく
					// 連結翻訳 … 再ビルド(&E) モジュール 'aaa'
					// 再翻訳後 … モジュールの再ビルド(&E) 'aaa'

					// ja ニーモニックを除去
					en = Mnemonics.removeJa(enWithMnemonic);

				} else {
					// en ニーモニックを除去
					en = Mnemonics.removeEn(enWithMnemonic);
				}
			}

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

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

			// 訳が見つかった場合
			if (!en.equals(ja)) {

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

					// 翻訳不要でも Javadoc、JsDoc 生成ウィザードの Package（除外指定されている）は
					// ニーモニック変換が必要 2009.02.05
					// -----
					// ただし、ニーモニック変換が不要な場合は除外 (例: Debugの構成...)。
					// 今のところハードコード。数が多いようであればプロパティー化。 2009.02.11
					if (en.equals("Debug")) {
						return enWithMnemonic;
					}
					ja = en;
				}
				// パッケージ条件プロパティー (書式 英語=%IF%日本語デフォルト,packages,,,\=日本語カスタム )
				else if (ja.startsWith(IF_KEY_PREFIX)) {

					PropertySet packageMap = packageConditionMap.get(en);
					if (packageMap == null) {
						packageMap = new PropertySet();
						packageConditionMap.put(en, packageMap);

						String pkgValues = ja.substring(IF_KEY_PREFIX.length());
						String[] pv = pkgValues.split(",");
						packageMap.put(null, pv[0]); // デフォルト

						for (int i = 1; i < pv.length; i++) {
							String[] kv = pv[i].split("=");
							packageMap.put(kv[0], kv[1]);
						}
					}
					ja = callHierarchy.findTrace(packageMap);
				}
			}

			// ニーモニック後処理 (日本用付与 / トレース除外や訳語が見つからなかった場合も行う)
			if (mnemonics) {
				ja = convertMnemonicEnToJa(enWithMnemonic, en, ja);
			}

			// 翻訳デバッグトレース
			if (traceConfig != null && traceConfig.pattern != null) {

				String target = traceConfig.isJa ? ja : en;
				if (target.matches(traceConfig.pattern)) {
					String s = System.currentTimeMillis() + "「" + enWithMnemonic + "」→「" + ja + "」";
					log.debug(new Throwable(s), "デバッグ翻訳追跡スタックトレース jointPoint:" + jointPoint);
					return s;
				}
			}

			return ja;

		} catch (Exception e) {

			log.error(e, "翻訳エラー enWithMnemonic=%s mnemonics=%s jointPoint=%s",
				enWithMnemonic, mnemonics, jointPoint);
			return enWithMnemonic;
		}
	}

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

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

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

			// 翻訳できなかった場合は空でキャッシュされている
			if (ja.isEmpty()) {
				ja = en;
			}
		} else {

			// 正規表現翻訳プロパティーから取得
			ja = getValueByRegex(new TranslationString(en));

			if (ja != null) {

				// 正規表現処理結果もキャッシュする
				// → 数値などとの組み合わせで多くなりすぎる可能性があるため、キャッシュ保存時に閾値超過クリア
				transCacheProp.put(en, ja);

			} else {

				// 翻訳済み (マルチ・バイトが含まれる) の場合
				String e = en;
				if (e.startsWith("<html>") && e.contains("<style>")) {
					e = e.replaceFirst("(?s)\\s*<style>.+</style>\\s*", ""); // フォント名 "メイリオ" などは検査除外
				}
				if (e.length() != e.getBytes().length && !StringUtils.containsAny(e, GLOBAL_MULTIBYTES_CHARS)) {
					ja = en;
				}
				// デフォルトの翻訳プロパティーから取得
				else {
					if (super.load()) {
						log.debug("キャッシュにないため翻訳プロパティーをロード: %s", Property.escapeKey(en));
					}
					String separator = jointPoint != null ? jointPoint.separator : null;
					ja = getValue(en, separator);
				}
				// キャッシュする (翻訳できなかった場合は空キャッシュ)
				transCacheProp.put(en, en.equals(ja) ? "" : ja);
			}
		}
		return ja;
	}

	/**
	 * ニーモニックを英語用から日本用に変換します。<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.convertEnToJa(enWithMnemonic, en, ja);
			mnemonicCacheProp.put(enWithMnemonic, jaWithMnemonic);
		}
		return jaWithMnemonic;
	}
}
