/*
 * 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.generator.nls;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import jp.sourceforge.mergedoc.pleiades.log.Logger;
import jp.sourceforge.mergedoc.pleiades.resource.Files;
import jp.sourceforge.mergedoc.pleiades.resource.PropertySet;

import org.apache.commons.io.IOUtils;

/**
 * プラグイン・リソースを保持するクラスです。
 * <p>
 * @author cypher256
 */
public class Plugin extends PropertySetUnit {

	/** ロガー */
	@SuppressWarnings("unused")
	private static final Logger log = Logger.getLogger(Plugin.class);

	/** プラグイン・ルート */
	private final File root;

	/** プラグイン ID */
	private String id;

	/**
	 * プラグインを構築します。
	 * <p>
	 * @param pluginFile プラグイン・フォルダーまたはプラグイン JAR ファイル
	 */
	public Plugin(File pluginFile) {

		this.root = pluginFile;
		if (pluginFile.isDirectory()) {
			loadFolderPlugin(pluginFile);
		} else {
			loadJarPlugin(pluginFile);
		}

		if (id == null) {
			throw new NullPointerException(
					"プラグイン ID が取得できませんでした。"
					+ pluginFile.getName());
		}
	}

	/**
	 * 指定されたプラグイン・フォルダーのリソースをロードします。
	 * <p>
	 * @param pluginFolder プラグイン・フォルダー
	 */
	protected void loadFolderPlugin(File pluginFolder) {

		InputStream is = null;
		try {

			File manifest = new File(pluginFolder, "META-INF/MANIFEST.MF");
			if (manifest.exists()) {

				// マニフェスト・ファイルがあれば、それからプラグイン ID を取得
				is = new BufferedInputStream(new FileInputStream(manifest));
				setIdInManifestFile(is);

			} else {

				File fragmentXML = new File(pluginFolder, "fragment.xml");
				if (fragmentXML.exists()) {

					// フラグメント XML があれば、それからプラグイン ID を取得
					is = new BufferedInputStream(new FileInputStream(fragmentXML));
					setIdInFragmentXML(is);

				} else {

					// フォルダー名からプラグイン ID を取得
					setIdInFileName(pluginFolder);
				}
			}

		} catch (IOException e) {
			throw new IllegalArgumentException(e);

		} finally {
			IOUtils.closeQuietly(is);
		}

		// フォルダーのリソースをロード
		loadFolder(pluginFolder);
	}

	/**
	 * 指定されたプラグイン jar ファイルをロードします。
	 * <p>
	 * @param jar プラグイン jar ファイル
	 */
	private void loadJarPlugin(File jar) {

		// jar ファイル名からプラグイン ID を取得
		setIdInFileName(jar);

		ZipInputStream zis = null;
		try {
			zis = new ZipInputStream(new BufferedInputStream(
					new FileInputStream(jar)));

			for (ZipEntry inEntry = null; (inEntry = zis.getNextEntry()) != null;) {

				String entryName = inEntry.getName();

				if (entryName.equals("META-INF/MANIFEST.MF")) {
					// プラグイン ID のセット（マニフェストファイル）
					setIdInManifestFile(zis);

				} else if (entryName.equals("fragment.xml")) {
					// プラグイン ID のセット（フラグメント XML）
					setIdInFragmentXML(zis);

				} else if (entryName.matches(".*?(.jar|.zip)$")) {

					// jar 内の zip
					ZipInputStream zisI = new ZipInputStream(zis);
			        for (ZipEntry inEntryI = null; (inEntryI = zisI.getNextEntry()) != null;) {

			        	// プロパティーを設定
			        	String name = getPluginPath(jar) + "/" + entryName + "/" + inEntryI.getName();
			        	setupProperties(name, zisI);
			        }

				} else {
					// プロパティーを設定
		        	String path = getPluginPath(jar) + "/" + entryName;
					setupProperties(path, zis);
				}
			}

		} catch (IOException e) {
			throw new IllegalStateException(e);

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

	/**
	 * 指定されたフォルダーに存在する言語パックを再帰的にロードし、
	 * プロパティーを生成します。
	 * <p>
	 * @param folder フォルダー
	 */
	protected void loadFolder(File folder) {

		File[] files = folder.listFiles();
		Arrays.sort(files);
		for (File file : files) {

			if (file.isDirectory()) {

				// 再帰
				loadFolder(file);

			} else if (file.getName().matches(".*?(.jar|.zip)$")) {

				// zip ファイルのロード
				loadZipFile(file);

			} else {

            	// プロパティーを設定
				setupProperties(getPluginPath(file), file);
			}
		}
	}

	/**
	 * 指定された zip ファイルをロードします。
	 * <p>
	 * @param zip zip ファイル
	 */
	private void loadZipFile(File zip) {

		ZipInputStream zis = null;
		try {
			zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(zip)));

            for (ZipEntry inEntry = null; (inEntry = zis.getNextEntry()) != null;) {

            	// プロパティーを設定
            	String path = getPluginPath(zip) + "/" + inEntry.getName();
            	setupProperties(path, zis);
            }

		} catch (IOException e) {
			throw new IllegalStateException(getPluginPath(zip), e);

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

	/**
	 * 指定されたファイルのプラグイン・パス文字列を取得します。
	 * @param file ファイル
	 * @return プラグイン・パス文字列
	 */
	protected String getPluginPath(File file) {
		return root.getName() + Files.relativePath(root, file).replace("\\", "/");
	}

	/**
	 * プロパティーを設定します。
	 * <p>
	 * @param path パス
	 * @param in プロパティーにセットするファイルまたは入力ストリーム
	 */
	protected void setupProperties(String path, Object in) {

		if (path.endsWith("_ja.properties")
				|| path.endsWith("_ja_JP.properties")) {
			// 日本語プロパティ

			processProperties(getJaPropertySet(), in);

		} else if (path.matches("^.*_\\w{2}\\.properties$")) {
			// 英語・日本語以外のプロパティ

		} else if (path.endsWith(".properties")) {
			// 英語プロパティ

			String rootName = root.getName();
			if (!rootName.matches("^.*?(\\.nls1_|\\.nl1_).*$")) {
				// ja で上書きされていた場合の対応（_ja があることが前提）
				processProperties(getEnPropertySet(), in);
			}
         }
	}

	/**
	 * プロパティー・ファイルを処理します。
	 * <p>
	 * @param prop 結果を格納するプロパティー
	 * @param in プロパティー・ファイルまたは入力ストリーム
	 */
	private void processProperties(PropertySet prop, Object in) {

		if (in instanceof File) {
			prop.load((File) in);
		} else if (in instanceof InputStream) {
			prop.load((InputStream) in);
		} else {
			throw new IllegalArgumentException(
				"予期しない型：" + in.getClass().getName());
		}
	}

	/**
	 * マニフェスト・ファイル入力ストリームからプラグイン ID をセットします。
	 * <p>
	 * @param is マニフェスト・ファイル入力ストリーム
	 * @throws IOException 入出力例外が発生した場合
	 */
	private void setIdInManifestFile(InputStream is) throws IOException {

		if (getId() != null) return;
		Attributes attributes = new Manifest(is).getMainAttributes();
		String value = attributes.getValue("Fragment-Host");
		if (value == null) {
			value = attributes.getValue("Bundle-SymbolicName");
		}
		if (value != null) {
			setId(value.split(";")[0].trim());
		}
	}

	/**
	 * フラグメント XML 入力ストリームからプラグイン ID をセットします。
	 * <p>
	 * @param is フラグメント XML ファイル入力ストリーム
	 * @throws IOException 入出力例外が発生した場合
	 */
	private void setIdInFragmentXML(InputStream is) throws IOException {

		if (getId() != null) return;
		String fragment = IOUtils.toString(is);
		setId(fragment.replaceFirst("(?s)^.*plugin-id=\"(.*?)\".*$", "$1"));
	}

	/**
	 * プラグイン・フォルダーまたはファイル名からプラグイン ID をセットします。
	 * <p>
	 * @param pluginFolder プラグイン・フォルダーまたはファイル名
	 */
	protected void setIdInFileName(File pluginFolder) {
		if (getId() != null) return;
		String name = pluginFolder.getName();
		name = name.replaceFirst("\\.(nls1|nl1|nl_ja)", "");
		name = name.replaceFirst("_[0-9]+\\.[0-9]+\\.[0-9]+.*$", "");
		setId(name);
	}

	/**
	 * プラグイン ID をセットします。
	 * <p>
	 * @param id プラグイン ID
	 */
	public void setId(String id) {
		this.id = id;
	}

	/**
	 * プラグイン ID を取得します。
	 * <p>
	 * @return プラグイン ID
	 */
	public String getId() {
		return id;
	}
}
