/*
 * 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.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

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.IOUtils;

/**
 * 変換済みクラス・キャッシュです。
 * <p>
 * @author cypher256
 */
public class TransformedClassCache {

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

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

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

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

	/** クラス・キャッシュ・ファイル */
	private final File cacheFile = Pleiades.getResourceFile(CacheFiles.TRANSFORMED_CLASS_CACHE);

	/** ロードされたクラス・マップ */
	private Map<String, byte[]> loadMap = new HashMap<String, byte[]>();

	/** 次回起動用に保管するクラス・マップ */
	private Map<String, byte[]> storeMap = new ConcurrentHashMap<String, byte[]>();

	/**
	 * 変換済みクラス・キャッシュを構築します。
	 */
	private TransformedClassCache() {
		loadMap = load();
	}

	/**
	 * クラス・キャッシュをロードします。
	 * @return クラス・マップ
	 */
	private Map<String, byte[]> load() {

		long start = System.nanoTime();
		Map<String, byte[]> map = new HashMap<String, byte[]>();

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

		} else {

			ZipInputStream in = null;
			try {
				in = new ZipInputStream(new BufferedInputStream(new FileInputStream(cacheFile)));
				ZipEntry inEntry = null;
				while ((inEntry = in.getNextEntry()) != null) {
					byte[] bytecode = IOUtils.toByteArray(in);
					map.put(inEntry.getName(), bytecode);
				}
				log.info("load   %6d エントリー %s", map.size(), cacheFile.getName());

			} catch (Exception e) {

				// キャッシュ破損、自己復元
				log.warn("変換済みクラス・キャッシュ %s の破損を検出。復元中... - %s", cacheFile, e.toString());
				cacheFile.delete();
				map.clear();

			} finally {
				IOUtils.closeQuietly(in);
			}
		}

		Analyses.end(getClass(), "<init>", start);
		return map;
	}

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

		if (storeMap == null || storeMap.size() == 0) {
			return;
		}
		Map<String, byte[]> map = load();
		ZipOutputStream out = null;
		try {
			out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(cacheFile)));
			out.setLevel(Deflater.BEST_SPEED);

			// ConcurrentModificationException 防止
			Map<String, byte[]> sMap = storeMap;
			storeMap = null;
			map.putAll(sMap);

			for (Entry<String, byte[]> entry : map.entrySet()) {
				out.putNextEntry(new ZipEntry(entry.getKey()));
				out.write(entry.getValue());
			}
			log.info("store  %6d エントリー %s", map.size(), cacheFile.getName());

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

		} finally {
			IOUtils.closeQuietly(out);
		}
	}

	/**
	 * キャッシュからバイトコードを取得します。
	 * メモリー節約のため、一度取得したエントリーは削除されます。
	 * <p>
	 * @param classId クラス識別子
	 * @return バイトコード
	 */
	public byte[] get(String classId) {
		return loadMap.remove(classId);
	}

	/**
	 * 次回起動時のキャッシュとして使用するバイトコードを追加します。
	 * このメソッドで追加したエントリーは store メソッドで保管されます。
	 * <p>
	 * @param classId クラス識別子
	 * @param bytecode バイトコード
	 */
	public void putNextLaunch(String classId, byte[] bytecode) {

		if (storeMap == null) {
			return;
		}
		if (storeMap.containsKey(classId)) {
			// 特に問題ないようなので出力しない 2012.06.24
			//log.debug("すでにキャッシュに存在します。" + classId);
			return;
		}
		storeMap.put(classId, bytecode);
	}

	/**
	 * 次回起動時のキャッシュに含まれるか判定します。
	 * <p>
	 * @param classId クラス識別子
	 * @return 含まれる場合は true
	 */
	public boolean contains(String classId) {
		if (storeMap == null) {
			return false;
		}
		return storeMap.containsKey(classId);
	}

	/**
	 * このキャッシュがシャットダウンされていないか判定します。
	 * @return シャットダウンされていない場合は true
	 */
	public boolean isActive() {
		return storeMap != null;
	}
}
