/*
 * 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;

import java.io.File;
import java.io.IOException;
import java.util.regex.Matcher;

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

/**
 * アプリケーション情報を参照するためのユーティリティクラスです。
 * <p>
 * @author cypher256
 */
public class Applications {

	/** Eclipse ホーム・ディレクトリー */
	private static File home;

	/** OSGI ホーム・ディレクトリー */
	private static File osgiHome;

	/** コンフィグレーション・パス */
	private static File pleiadesConfigurationPath;

	/** Eclipse のバージョン */
	private static String version;

	/**
	 * このクラスを初期化します。
	 */
	public static void init() throws IOException {

		File configIniFile = getConfigIniFile("configuration");

		// Eclipse の場合
		if (configIniFile != null) {
			pleiadesConfigurationPath = getPleiadesConfigPath(configIniFile);
		}
		// Eclipse ではない場合 (IDEA など)
		else {
			File packageDir = Files.getPackageDirectory();
			if (isWritable(packageDir)) {
				pleiadesConfigurationPath = new File(packageDir, "cache");
			} else {
				pleiadesConfigurationPath = getUserConfigPath(packageDir.getParentFile().getParentFile());
			}
		}
		pleiadesConfigurationPath.mkdirs();
	}

	/**
	 * config.ini ファイルを取得します。
	 * @return config.ini ファイル
	 */
	private static File getConfigIniFile(String configurationDir) {

		final String CONFIG_INI = "config.ini";
		final String configDirIni = configurationDir + "/" + CONFIG_INI;

		// ユーザの作業ディレクトリにある場合
		File configIniFile = new File(System.getProperty("user.dir"), configDirIni);
		if (configIniFile.exists()) {
			return configIniFile;
		}
		File eclipseDir = Files.getPackageDirectory().getParentFile().getParentFile();

		// config ディレクトリがデフォルトの名前の場合の検索 (ディレクトリー＋ファイル名で検索)
		for (File dir = eclipseDir; dir != null; dir = dir.getParentFile()) {
			configIniFile = new File(dir, configDirIni);
			if (configIniFile.exists()) {
				return configIniFile;
			}
		}

		// config ディレクトリが -configuration で変更されている場合の検索 (ファイル名だけで検索)
		for (File dir = eclipseDir; dir != null; dir = dir.getParentFile()) {
			for (File d : dir.listFiles(Files.createDirectoryFilter())) {
				configIniFile = new File(d, CONFIG_INI);
				if (configIniFile.exists()) {
					return configIniFile;
				}
			}
		}

		// Eclipse からの実行ではない場合
		return null;
	}

	/**
	 * Eclipse 設定から Pleiades 構成保存用のパスを取得します。
	 * @param configIniFile config.ini ファイル
	 * @return Pleiades 構成保存パス
	 */
	private static File getPleiadesConfigPath(File configIniFile) {

		// config.ini の位置を元に Eclipse ホーム・ディレクトリーを取得
		File defaultConfigPath = configIniFile.getParentFile();
		home = defaultConfigPath.getParentFile();
		PropertySet configIni = new PropertySet(configIniFile);

		// バンドル・ホームを取得
		// 通常は ホーム・ディレクトリーと同じですが、p2agent を使用した場合、
		// インストール時に指定したバンドル・プール・ロケーションとなり、
		// このディレクトリー配下の plugins に基本的なプラグイン群が格納されます。

		String osgiJarPath = configIni.get("osgi.framework");
		if (osgiJarPath != null) {
			File jarFile = new File(osgiJarPath.replace("file:", ""));
			if (jarFile.exists()) {
				osgiHome = jarFile.getParentFile().getParentFile();
			}
		}
		if (osgiHome == null) {
			osgiHome = home;
		}

		// config.ini からユーザー・コンフィグレーション・パスの取得
		final String CONFIG_AREA_KEY = "osgi.configuration.area";
		String configArea = configIni.get(CONFIG_AREA_KEY);
		if (configArea == null) {
			configArea = System.getProperty(CONFIG_AREA_KEY);
		}
		File userConfigPath = null;
		if (configArea != null) {
			Matcher mat = PatternCache.get("^@([\\w\\.]+)(.+)$").matcher(configArea);
			if (mat.find()) {
				String base = System.getProperty(mat.group(1));
				if (base != null) {
					File baseDir = new File(base);
					if (baseDir.exists()) {
						String childPath = mat.group(2);
						userConfigPath = new File(baseDir, childPath);
					}
				}
			}
		}

		// config.ini に指定がある場合
		if (userConfigPath != null) {
			return new File(userConfigPath, Pleiades.PACKAGE_NAME);
		}
		// config.ini に指定がない場合
		else {
			// デフォルトの configuration ディレクトリ (<インストールディレクトリ>/configuration/)
			File storePath = new File(defaultConfigPath, Pleiades.PACKAGE_NAME);
			storePath.mkdirs();
			if (isWritable(storePath)) {
				return storePath;
			}
			// 書き込み権が無い場合はユーザーホームディレクトリ
			return getUserConfigPath(home);
		}
	}

	/**
	 * 指定したディレクトリにファイルが書き込めるか判定します。
	 * @param storePath 保存するディレクトリ
	 * @return 書き込める場合は true
	 */
	private static boolean isWritable(File storePath) {

		// 書き込み権限チェック (File#canWrite では正常に判定できない)
		File checkWritable = new File(storePath, ".check-writable");
		try {
			checkWritable.createNewFile();
			if (checkWritable.delete()) {
				return true;
			}
		} catch (Exception e) {
			// 書き込み不可
		}
		return false;
	}

	/**
	 * 書き込み権無しに対応するために、ユーザディレクトリ配下のディレクトリを取得する。
	 * @param appDir ディレクトリ
	 * @return ディレクリ名として使用できる文字列
	 */
	private static File getUserConfigPath(File appDir) {

		// Windows UAC 書き込み不可や Linux、Mac のマルチユーザ書き込み権なしに対応
		// <ユーザホーム>/.eclipse/<パッケージ名>/<インストールパスの - 区切り>/
		File base = new File(System.getProperty("user.home"), ".pleiades");

		// ディレクトリ名として使用できる文字列に変換
		String appPath = appDir.getPath().replaceAll("[/:\\\\]+", "-").replaceFirst("^-+", "");
		return new File(base, appPath);
	}

	/**
	 * Eclipse 製品のホーム・ディレクトリーを取得します。
	 * @return Eclipse ホーム・ディレクトリー。Eclipse ではない場合 (IDEA など) は null。
	 */
	public static File getHome() {
		return home;
	}

	/**
	 * Eclipse 製品の OSGI ホーム・ディレクトリーを取得します。
	 * @return Eclipse OSGI ホーム・ディレクトリー。Eclipse ではない場合 (IDEA など) は null。
	 */
	public static File getOsgiHome() {
		return osgiHome;
	}

	/**
	 * Pleiades 構成保存パスを取得します。
	 * @return Pleiades 構成保存パス
	 */
	public static File getPleiadesConfigurationPath() {
		return pleiadesConfigurationPath;
	}

	/**
	 * Eclipse のバージョン文字列を取得します。
	 * @return Eclipse バージョン文字列。取得できない場合は空文字。
	 */
	public static synchronized String getVersion() {

		if (version != null) {
			return version;
		}
		version = "";
		if (osgiHome == null) {
			return version;
		}

		// Eclipse のバージョン取得
		File featuresFolder = new File(osgiHome, "features");
		if (!featuresFolder.exists()) {
			return version;
		}
		for (File folder : featuresFolder.listFiles()) {
			String folderName = folder.getName();
			if (folderName.startsWith("org.eclipse.platform_")) {
				version = folderName.replaceFirst(".*?_(\\d+\\.\\d+).*", "$1");
				break;
			}
		}
		return version;
	}

	/**
	 * 前回起動時から Eclipse プラグインが更新されているか判定します。
	 * @param targetDirs ターゲットディレクトリ
	 * @return 1 つでも更新されている場合は true
	 */
	public static boolean isUpdated(File... targetDirs) throws IOException {

		if (targetDirs.length == 0) {
			throw new IllegalStateException("ターゲットディレクトリが指定されていません。");
		}
		long start = System.nanoTime();
		File pluginModifiedFile = new File(pleiadesConfigurationPath, ".plugin-modified");
		long prevLastModified = pluginModifiedFile.lastModified();
		long lastModified = prevLastModified;

		// 指定された plugins や dropins ディレクトリの変更を検知
		for (File dir : targetDirs) {
			lastModified = getLastModified(dir, lastModified);
		}

		// 翻訳プロパティファイルや pleiades-config.xml の変更を検知
		lastModified = getLastModified(Files.conf(), lastModified);

		Logger log = Logger.getLogger(Applications.class);
		log.info("ファイル更新最終時刻 (前回): %1$tF %1$tT", prevLastModified);
		log.info("ファイル更新最終時刻 (今回): %1$tF %1$tT", lastModified);

		boolean isUpdated = false;
		if (lastModified > prevLastModified) {
			pluginModifiedFile.createNewFile();
			pluginModifiedFile.setLastModified(lastModified);
			isUpdated = true;
		}
		Analyses.end(Applications.class, "isPluginUpdated", start);
		return isUpdated;
	}

	/**
	 * 指定したディレクトリーに含まれるディレクトリーの最新の更新時刻を取得します。
	 * @param parentDir 親ディレクトリー
	 * @param latest 最新時刻
	 * @return 最新時刻。一番新しい時刻を取得するために途中で止めない。
	 */
	private static long getLastModified(File parentDir, long latest) {

		if (!parentDir.exists()) {
			return latest;
		}
		String parentName = parentDir.getName();

		// フィーチャーディレクトリはチェックしない
		if (parentName.equals("features")) {
			return latest;
		}

		for (File file : parentDir.listFiles()) {

			if (file.isFile()) {
				latest = max(file, latest);
			}
			else {
				File dir = file;

				if (parentName.equals("plugins") || parentName.equals("lib")) {
					latest = max(dir, latest); // 親がプラグインディレクトリの場合は再帰しないで自身のみチェック
				} else {
					latest = getLastModified(dir, latest); // 再帰
				}
			}
		}
		return latest;
	}

	/**
	 * ファイルの更新時刻をチェックして、最新時刻と比較して新しいほうを返します。
	 * @param file 更新時刻をチェックするファイル
	 * @param latest 最新時刻
	 * @return 比較して新しいほうの時刻
	 */
	private static long max(File file, long latest) {

		long modified = file.lastModified();
		if (modified > latest) {
			latest = modified;
			Logger log = Logger.getLogger(Applications.class);
			log.info("ファイル更新検出: %1$tF %1$tT %2$s", latest, file.getPath());
		}
		return latest;
	}
}
