package jp.sourceforge.mergedoc.pleiades.runtime.resource;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

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

/**
 * 参照比較でキャッシュされる翻訳辞書です。
 * 別スレッドかつ参照が同一の英語リソースで呼び出された場合は異なる結果を返す可能性がありますが、
 * 通常利用で、ほぼ発生しないため、パフォーマンスを優先し、synchronized で同期化していません。
 * <p>
 * @author cypher256
 */
public class IdentityTranslationCache {

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

	/** 翻訳辞書 */
	private static final RuntimeTranslationDictionary dictionary = RuntimeTranslationDictionary.getInstance();

	/** 直前の日本語リソース文字列キャッシュ */
	private String jaCache;

	/** 直前の英語リソース文字列キャッシュ */
	private String enCache;

	/** LRU キャッシュ最大容量 */
	private static final int LRU_CACHE_CAPACITY = 100;

	/** LRU キャッシュキークラス (対象オブジェクト $0 を == で参照比較するためのラッパー) */
	private static class IdentityKey {

		private final Object target;

		public IdentityKey(Object target) {
			this.target = target;
		}

		@Override
		public boolean equals(Object obj) {
			return (obj instanceof IdentityKey) && ((IdentityKey) obj).target == target;
		}

		@Override
		public int hashCode() {
			return target.hashCode();
		}
	}

	/** LRU キャッシュとなるマップ (キー：英語、値：日本語) */
	private static Map<IdentityKey, Map<String, String>> targetObjectCache =
			new LinkedHashMap<IdentityKey, Map<String,String>>(LRU_CACHE_CAPACITY, 0.75f, true) {

		protected boolean removeEldestEntry(Map.Entry<IdentityKey, Map<String, String>> eldest) {
			boolean removes = size() > LRU_CACHE_CAPACITY;
			return removes;
		}
	};

	/**
	 * LRU キャッシュを取得します。
	 * <p>
	 * @param en 英語リソース文字列 (ニーモニック含む) : Not null
	 * @return LRU キャッシュとなるマップ (キー：英語、値：日本語)
	 */
	private Map<String, String> getCache(String en, RuntimeJointPoint jointPoint) {

		if (jointPoint == null) {
			return null;
		}
		IdentityKey targetKey = new IdentityKey(jointPoint.getTarget());
		Map<String, String> enJaMap = targetObjectCache.get(targetKey);
		if (enJaMap == null) {
			enJaMap = new HashMap<String, String>();
			targetObjectCache.put(targetKey, enJaMap);
		}
		return enJaMap;
	}

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

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

	/**
	 * 指定した英語リソース文字列から日本語リソースを探します。
	 * ニーモニックは日本用に変換されます。
	 * <p>
	 * @param en 英語リソース文字列 (ニーモニック含む) : Not null
	 * @param jointPoint ジョイント・ポイント
	 * @param mnemonics ニーモニックを処理する場合は true
	 * @return 日本語リソース文字列 (ニーモニック含む)。
	 */
	private String translate(String en, RuntimeJointPoint jointPoint, boolean mnemonics) {

		// 直前の翻訳対象文字列の参照 (中身ではない) が同じ場合はキャッシュを返す
		if (en == enCache) {
			return jaCache;
		}
		if (en.isEmpty()) {
			return en;
		}

		// 対象オブジェクトに紐づくキャッシュ
		Map<String, String> cache = getCache(en, jointPoint);
		if (cache != null) {
			String ja = cache.get(en);
			if (ja != null) {
				return ja;
			}
		}

		// 想定外の大きな文字列が渡されたときのパフォーマンス劣化を抑止 (インスペクションの説明は 3000 ぐらい)
		if (en.length() > 6000) {
			if (log.isDebugEnabled()) {
				log.debug(
					"翻訳対象が大きすぎるため無視 length=%d %s %s",
					en.length(), jointPoint, en.substring(0, 2000).replaceAll("\n", "\\\\n")
				);
			}
			return en;
		}

		// 高速化のため、単純に判定可能な場合はニーモニック処理フラグを OFF にする
		if (mnemonics) {

			// 文字数が多い場合はニーモニック無しとみなす (パフォーマンス対応)
			if (en.length() > 300) {
				mnemonics = false;
			}
			// HTML とみなされる場合
			else if (en.charAt(0) == '<' && en.startsWith("<html")) {
				mnemonics = false;
			}
		}

		jaCache = dictionary.lookup(en, jointPoint, mnemonics);
		enCache = en;
		if (cache != null) {
			cache.put(enCache, jaCache);
		}
		return jaCache;
	}

	/**
	 * ニーモニックがアンダースコアのリソース文字列を翻訳します。
	 * <p>
	 * @param en 日本用ニーモニックが含まれたリソース文字列
	 * @param jointPoint ジョイント・ポイント
	 * @return 翻訳後文字列。翻訳できない場合は引数と同じ。
	 */
	public String translateUnderscoreMnemonic(String en, RuntimeJointPoint jointPoint) {

		// 直前の翻訳対象文字列の参照 (中身ではない) が同じ場合はキャッシュを返す
		if (en == enCache) {
			return jaCache;
		}
		if (en.isEmpty()) {
			return en;
		}

		// 対象オブジェクトに紐づくキャッシュ
		Map<String, String> cache = getCache(en, jointPoint);
		if (cache != null) {
			String ja = cache.get(en);
			if (ja != null) {
				return ja;
			}
		}

		char origin = Character.MIN_VALUE;
		final char ESC = '\u001B';

		// IDEA 系でニーモニックを表す ESC 制御文字
		if (en.indexOf(ESC) > -1) {
			origin = ESC;
		}

		// IDEA の特殊な && を除去 (ニーモニックも動作しないため除去)
		// 例: Include &non-&&project files
		else if (en.contains("&&") && en.matches(".+&&\\w.+")) {

			log.debug("特殊な && を除去 '%s'", en);
			en = en.replaceAll("&", "");
		}

		// Swing ニーモニックを表すアンダースコア
		else if (

			en.contains("_") &&

			// パスと思われる文字列は除外
			StringUtils.countMatches(en, "/") <= 2 &&
			StringUtils.countMatches(en, "\\") <= 2 &&

			// シングルクォートで囲まれていない部分の _ の数が 2 つ以上は除外
			// ○ --- 'xxx_y_y_y' Bu_ild
			// × --- 'xxxyyy' Bu_il_d
			countNonQuoted(en, '_') < 2 &&

			// 文章中に *_HOME がある場合は除外 (Use JAVA_HOME など)
			!en.matches(".+[A-Z]_HOME.*") &&

			// 大文字スネークケースは定数と判断
			!en.matches("[A-Z][A-Z_]+[A-Z]") &&

			// & ニーモニックが含まれていると戻せないため除外
			!en.matches(".*&\\w.*") &&

			// _ ニーモニック
			en.matches(".*_\\w.*")
		) {
			origin = '_';
		}

		// & 以外のニーモニックなし -> 通常のニーモニック翻訳 (ニーモニックの & が入ってくるため)
		if (origin == Character.MIN_VALUE) {

			 String ja = translateMnemonic(en, jointPoint);

			 // en に & あり、かつ、ja にない場合、末尾に & を付ける
			// ▶ R&un 'add_ons' → 実行 'addons' にならないようにする
			// ▶ IDEA Mac で & がない場合、_ がニーモニックとして扱われて消えてしまうのを回避
			 if (en.contains("&") && !ja.contains("&")) {
				ja += "&";
			 }
			 return ja;
		}

		// ニーモニックを origin から & へ置換して翻訳して戻す (元に戻さないと不具合が出る場合があるため)
		String enAmp = replaceNonQuoted(en, origin, '&');
		String jaAmp = dictionary.lookup(enAmp, jointPoint, true);

		if (enAmp.equals(jaAmp)) {
			jaCache = en;
		} else {
			jaCache = replaceNonQuoted(jaAmp, '&', origin);
		}
		enCache = en;
		if (cache != null) {
			cache.put(enCache, jaCache);
		}
		return jaCache;

	}

	/**
	 * シングルクォートに囲まれていない部分を文字をカウントします。
	 * @param s 対象文字列
	 * @param ch カウントする文字
	 * @return 文字数
	 */
	private int countNonQuoted(String s, char ch) {

		int count = 0;
		boolean insideQuote = false;

		for (int i = 0; i < s.length(); i++) {
			char c = s.charAt(i);
			if (c == '\'') {
				insideQuote = !insideQuote;
			}
			if (!insideQuote && c == ch) {
				count++;
			}
		}
		return count;
	}

	/**
	 * シングルクォートに囲まれていない部分で最初に見つかった char を置換します。String#replaceFirst より高速です。
	 * <p>
	 * @param s 対象文字列
	 * @param oldChar 置換前文字
	 * @param newChar 置換後文字
	 * @return 置換後の文字列
	 */
	private String replaceNonQuoted(String s, char oldChar, char newChar) {

		char[] chars = s.toCharArray();
		boolean insideQuote = false;

		for (int i = 0; i < chars.length - 1; i++) {
			char c = chars[i];
			if (c == '\'') {
				insideQuote = !insideQuote;
			}
			if (!insideQuote && c == oldChar) {
				char next = chars[i + 1];
				if (String.valueOf(next).matches("\\w")) {
					chars[i] = newChar;
					break;
				}
			}
		}
		return new String(chars);
	}
}
