/*
 * 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 java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import jp.sourceforge.mergedoc.pleiades.Pleiades;
import jp.sourceforge.mergedoc.pleiades.aspect.Analyses;
import jp.sourceforge.mergedoc.pleiades.log.Logger;

import org.apache.commons.io.FileUtils;

/**
 * 変換除外クラス名キャッシュです。
 * <p>
 * @author cypher256
 */
public class ExcludeClassNameCache {

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

	/** 変換除外クラス名キャッシュ・ファイル */
	public static final File cacheFile = Pleiades.getResourceFile(CacheFiles.EXCLUDE_CLASS_LIST);

	/** このクラスのシングルトン・インスタンス */
	private static final ExcludeClassNameCache singleton = new ExcludeClassNameCache();

	/** キャッシュしない接頭辞 */
	private static final String NO_CACHE_PREFFIX = "org.apache.derby.exe.ac601a400fx011fx";

	/**
	 * 変換除外クラス名キャッシュ・オブジェクトを取得します。
	 * <p>
	 * @return 変換除外クラス名キャッシュ・オブジェクト
	 */
	public static ExcludeClassNameCache getInstance() {
		return singleton;
	}

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

	/** ロードされた変換除外クラス・セット */
	private Set<String> loadedSet = new HashSet<String>();

	/** 次回起動用に保管する変換除外クラス・セット */
	private Set<String> storeSet = Collections.synchronizedSet(new HashSet<String>());

	/**
	 * 変換除外クラス名キャッシュを構築します。
	 */
	private ExcludeClassNameCache() {
		loadedSet = load();
	}

	/**
	 * 変換除外クラス名リストをロードします。
	 * @return 変換除外クラス名セット
	 */
	@SuppressWarnings("unchecked")
	private Set<String> load() {

		long start = System.nanoTime();
		Set<String> set = new HashSet<String>();

		if (!cacheFile.exists()) {
			log.info(cacheFile.getName() + " が存在しません。");

		} else {

			try {
				set.addAll(FileUtils.readLines(cacheFile, null));
				log.info("load  %6d エントリー %s", set.size(), cacheFile.getName());

			} catch (Exception e) {

				// キャッシュ破損、自己復元
				log.warn("除外クラス名キャッシュ %s の破損を検出。復元中... - %s", cacheFile, e.toString());
				cacheFile.delete();
				set.clear();
			}
		}

		Analyses.end(ExcludeClassNameCache.class, "<init>", start);
		return set;
	}

	/**
	 * シャットダウンします。
	 * 追加されたクラス名はキャッシュとして永続化されます。
	 */
	public void shutdown() {
		// AOP 自動計測

		if (storeSet == null || storeSet.size() == 0) {
			return;
		}
		Set<String> set = load();
		try {
			// ConcurrentModificationException 防止
			Set<String> sSet = storeSet;
			storeSet = null;
			for (String className : sSet) {
				if (!className.startsWith(NO_CACHE_PREFFIX)) {
					set.add(className);
				}
			}

			FileUtils.writeLines(cacheFile, null, set);
			log.info("store %6d エントリー %s", set.size(), cacheFile.getName());

		} catch (IOException e) {
			log.error(e, cacheFile.getName() + " の保管に失敗しました。");
		}
	}

	/**
	 * AOP 変換から除外するクラス名を追加します。
	 * このメソッドで追加したエントリーは store メソッドで保管されます。
	 * <p>
	 * @param className クラス名
	 */
	public void addNextLaunch(String className) {

		if (storeSet == null) {
			return;
		}
		if (storeSet.contains(className)) {

			// クラス名の重複は無視。ただし Eclipse などは異なるプラグインで異なるクラスとしてロードできるため
			// 問題がある場合は、プラグイン ID などをキーに加えることを考慮する。
			//log.debug("すでにキャッシュに存在します。" + className);
			return;
		}
		storeSet.add(className);
	}

	/**
	 * AOP 変換から除外するクラス名が存在するか判定します。
	 * メモリー節約のため、一度判定したエントリーは削除されます。
	 * <p>
	 * @param className クラス名
	 * @return 指定されたクラス名が存在する場合は true
	 */
	public boolean contains(String className) {
		return loadedSet.remove(className) || className.startsWith(NO_CACHE_PREFFIX);
	}
}
