package charactermanaj.model;

import java.awt.Color;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import charactermanaj.util.ApplicationLogHandler;
import charactermanaj.util.BeanPropertiesUtilities;
import charactermanaj.util.ConfigurationDirUtilities;

/**
 * アプリケーションの全域にわたる設定.<br>
 * アプリケーション設定は、クラスパス上のリソース、コートベース直下のappConfig.xml、ユーザーごとのappConfig.xmlの順に読み込まれます.<br>
 * @author seraphy
 */
public final class AppConfig {

	/**
	 * アプリケーション設定ファイルの名前
	 */
	private static final String CONFIG_NAME = "appConfig.xml";

	/**
	 * アプリケーションのバージョン
	 */
	public static final String APP_VERSION = "1.0";
	
	/**
	 * 全ユーザー用キャラクターディレクトリのシステムプロパティのキー名.<br>
	 */
	public static final String COMMON_CHARACTER_DIR_PROPERTY_NAME = "character.dir";
	
	
	/**
	 * ロガー
	 */
	private static final Logger logger = Logger.getLogger(AppConfig.class.getName());
	
	
	/**
	 * シングルトンインスタンス
	 */
	private static final AppConfig singleton = new AppConfig();
	
	
	/**
	 * インスタンスを取得する.
	 * @return インスタンス
	 */
	public static AppConfig getInstance() {
		return singleton;
	}

	/**
	 * プライベートコンストラクタ
	 */
	private AppConfig() {
		super();
	}
	
	
	/**
	 * プロパティをロードする.<br>
	 * リソース上の/appConfig.xml、コードベース下のappConfig.xml、アプリケーションデータ保存先のappConfig.xmlの順に読み取る.<br>
	 * 存在しないか、読み取りに失敗した場合は、該当ファイルはスキップされる.<br>
	 */
	public void loadConfig() {
		Properties config = new Properties();
		try {
			File codeBase = ConfigurationDirUtilities.getApplicationBaseDir();
			File userDataDir = ConfigurationDirUtilities.getUserDataDir();
			URL[] urls = {
					getClass().getResource("/" + CONFIG_NAME),
					new File(codeBase, CONFIG_NAME).getCanonicalFile().toURL(),
					new File(userDataDir, CONFIG_NAME).toURL(),
			};
			for (URL url : urls) {
				if (url == null) {
					continue; // リソースがない場合はnullになる
				}
				// ファイルの実在チェック (チェックできる場合のみ)
				if (url.getProtocol().equals("file")) {
					File file = new File(url.getPath());
					if (!file.exists()) {
						logger.log(Level.INFO, "appConfig.xml is not found.:" + file);
						continue;
					}
				}
				// appConfig.xmlの読み込みを行う.
				// Properties#loadFromXML() はXMLからキーを読み取り、既存のキーに対して上書きする.
				// XMLに存在しないキーは読み込み前のままなので、繰り返し呼び出すことで「重ね合わせ」することができる.
				try {
					InputStream is = url.openStream();
					try {
						config.loadFromXML(is);
						logger.log(Level.INFO, "appConfig.xml is loaded.:" + url);
					} finally {
						is.close();
					}
					
				} catch (FileNotFoundException ex) {
					logger.log(Level.INFO, "appConfig.xml is not found.: " + url, ex);
					// 無視する (無い場合は十分にありえるので「情報」レベルでログ。)
				} catch (Exception ex) {
					logger.log(Level.WARNING, "appConfig.xml loading failed.: " + url, ex);
					// 無視する
				}
			}
		} catch (Exception ex) {
			throw new RuntimeException("appConfig.xml loading failed.", ex);
		}
		BeanPropertiesUtilities.loadFromProperties(this, config);
	}

	/**
	 * プロパティをアプリケーションデータの保存先に保存する.
	 * @throws IOException 保存に失敗した場合
	 */
	public void saveConfig() throws IOException {
		Properties config = getProperties();
		
		File userDataDir = ConfigurationDirUtilities.getUserDataDir();
		File configStore = new File(userDataDir, CONFIG_NAME);
		OutputStream os = new BufferedOutputStream(new FileOutputStream(configStore));
		try {
			config.storeToXML(os, CONFIG_NAME, "UTF-8");
		} finally {
			os.close();
		}
	}
	
	/**
	 * Propertiesの値を設定した場合に設定できない項目があるかチェックする.<br>
	 * このメソッドを呼び出しても、アプリケーション設定自身は何も影響されない.<br>
	 * @param props 適用するプロパティ
	 * @return 設定できなかったプロパティキーのコレクション、問題なければ空が返される.
	 */
	public static Set<String> checkProperties(Properties props) {
		if (props == null) {
			throw new IllegalArgumentException();
		}
		AppConfig dummy = new AppConfig(); // アプリケーションから参照されないダミーのインスタンスを作成する.
		return BeanPropertiesUtilities.loadFromProperties(dummy, props);
	}

	/**
	 * Propertiesの値で設定を更新する.<br>
	 * @param props 適用するプロパティ
	 * @return 設定できなかったプロパティキーのコレクション、問題なければ空が返される.
	 */
	public Set<String> update(Properties props) {
		if (props == null) {
			throw new IllegalArgumentException();
		}
		return BeanPropertiesUtilities.loadFromProperties(this, props);
	}

	/**
	 * このアプリケーション設定をプロパティに書き出して返します.<br>
	 * @return プロパティ
	 */
	public Properties getProperties() {
		Properties config = new Properties();
		BeanPropertiesUtilities.saveToProperties(this, config);
		return config;
	}
	
	/**
	 * 全ユーザー共通のキャラクターデータディレクトリを取得する.<br>
	 * 全ユーザー共通のキャラクターデータディレクトリが実在しない場合はnullを返す.<br>
	 * アプリケーション設定、システムプロパティ「character.dir」、アプリケーションディレクトリ上の「characters」の優先順で検索される.<br>
	 * @return 全ユーザー共通のキャラクターデータディレクトリ、またはnull
	 */
	public File getSystemCharactersDir() {
		
		File applicationBaseDir = ConfigurationDirUtilities.getApplicationBaseDir();
		File[] candidates = new File[] {
				getAbsoluteFile(applicationBaseDir, getCommonCharacterDataDir()),
				getAbsoluteFile(applicationBaseDir, System.getProperty(COMMON_CHARACTER_DIR_PROPERTY_NAME)),
				getAbsoluteFile(applicationBaseDir, "characters"),
		};
		File systemCharacterDir = null;
		for (File dir : candidates) {
			// 候補を順に検査し最初に合格したディレクトリを採用する
			if (dir == null) {
				continue;
			}
			if (dir.exists() && dir.isDirectory()) {
				systemCharacterDir = dir;
				break;
			}
		}
		return systemCharacterDir;
	}
	
	/**
	 * 指定したファイルが絶対パスであれば、それを返す.<br>
	 * 絶対パスでなければベースディレクトリを親とした相対パスとして正規化し、その絶対パスを返す.<br>
	 * fileがnullまたは空文字の場合はnullを返す.<br>
	 * baseDirとfileを連結し正規化するときにエラーが発生した場合はログに記録しnullを返す.<br>
	 * @param baseDir ベースディレクトリ、fileが相対パスであれば、fileの親となる.
	 * @param file ファイル、nullまたは空文字も可
	 * @return 絶対パス、もしくはnull
	 */
	protected File getAbsoluteFile(File baseDir, String file) {
		if (baseDir == null) {
			throw new IllegalArgumentException();
		}
		if (file == null || file.trim().length() == 0) {
			return null;
		}
		File result = new File(file);
		if (!result.isAbsolute()) {
			try {
				// ファイル名が絶対パスでない場合はベースディレクトリからの相対にする
				result = new File(baseDir, file.trim()).getCanonicalFile();
				
			} catch (IOException ex) {
				logger.log(Level.SEVERE, "invalid dir: base=" + baseDir + "/file=" + file, ex);
				return null;
			}
		}
		return result;
	}

	/**
	 * ユーザー固有のキャラクターデータディレクトリを取得する.<br>
	 * ユーザー固有のキャラクターディレクトリがまだ存在しない場合は作成される.<br>
	 * @return ユーザー固有のキャラクターデータディレクトリ
	 */
	public File getUserCharactersDir() {
		File characterBaseDir = new File(ConfigurationDirUtilities.getUserDataDir(), "characters");
		if (!characterBaseDir.exists()) {
			if (!characterBaseDir.mkdirs()) {
				logger.log(Level.WARNING, "can't create the charatcer base directory. " + characterBaseDir);
			}
		}
		return characterBaseDir;
	}
	
	/**
	 * 全ユーザー対象のキャラクターディレクトリを明示的に指定する場合のフォルダパス.<br>
	 * 相対パス指定の場合はアプリケーションディレクトリからの相対となる.<br>
	 * nullまたは空文字はシステムプロパティ、もしくはデフォルトディレクトリを使用することを意味する.<br>
	 * @return ディレクトリ
	 */
	public String getCommonCharacterDataDir() {
		return commonCharacterDataDir;
	}
	
	public void setCommonCharacterDataDir(String commonCharacterDataDir) {
		if (commonCharacterDataDir == null) {
			commonCharacterDataDir = "";
		}
		this.commonCharacterDataDir = commonCharacterDataDir;
	}
	
	private String commonCharacterDataDir = "";
	
	
	/**
	 * デフォルトプロファイル名を取得する.
	 * @return デフォルトプロファイル名
	 */
	public String getDefaultProfileId() {
		return defaultProfileId;
	}
	
	public void setDefaultProfileId(String defaultProfileId) {
		if (defaultProfileId == null || defaultProfileId.trim().length() == 0) {
			throw new IllegalArgumentException();
		}
		this.defaultProfileId = defaultProfileId.trim();
	}
	
	private String defaultProfileId = "default";

	
	
	/**
	 * プロファイル選択ダイアログのプロファイルのサンプルイメージの背景色
	 * @return サンプルイメージの背景色
	 */
	public Color getSampleImageBgColor() {
		return sampleImageBgColor;
	}
	
	public void setSampleImageBgColor(Color sampleImageBgColor) {
		if (sampleImageBgColor == null) {
			throw new IllegalArgumentException();
		}
		this.sampleImageBgColor = sampleImageBgColor;
	}
	
	private Color sampleImageBgColor = Color.white;
	

	/**
	 * デフォルトのイメージ背景色を取得する.
	 * @return デフォルトのイメージ背景色
	 */
	public Color getDefaultImageBgColor() {
		return defaultImageBgColor;
	}
	
	public void setDefaultImageBgColor(Color defaultImageBgColor) {
		if (defaultImageBgColor == null) {
			throw new IllegalArgumentException();
		}
		this.defaultImageBgColor = defaultImageBgColor;
	}
	
	private Color defaultImageBgColor = Color.white;

	/**
	 * 使用中アイテムの背景色を取得する.
	 * @return 使用中アイテムの背景色
	 */
	public Color getCheckedItemBgColor() {
		return checkedItemBgColor;
	}
	
	public void setCheckedItemBgColor(Color checkedItemBgColor) {
		if (checkedItemBgColor == null) {
			throw new IllegalArgumentException();
		}
		this.checkedItemBgColor = checkedItemBgColor;
	}
	
	private Color checkedItemBgColor = Color.cyan.brighter();
	
	
	/**
	 *　選択アイテムの背景色を取得する 
	 * @return 選択アイテムの背景色
	 */
	public Color getSelectedItemBgColor() {
		return selectedItemBgColor;
	}
	
	public void setSelectedItemBgColor(Color selectedItemBgColor) {
		this.selectedItemBgColor = selectedItemBgColor;
	}
	
	private Color selectedItemBgColor = Color.orange;
	
	/**
	 * 不備のあるデータ行の背景色を取得する.
	 * @return 不備のあるデータ行の背景色
	 */
	public Color getInvalidBgColor() {
		return invalidBgColor;
	}
	
	public void setInvalidBgColor(Color invalidBgColor) {
		if (invalidBgColor == null) {
			throw new IllegalArgumentException();
		}
		this.invalidBgColor = invalidBgColor;
	}
	
	private Color invalidBgColor = Color.red.brighter().brighter();

	/**
	 * JPEG画像変換時の圧縮率を取得する.
	 * @return 圧縮率
	 */
	public float getCompressionQuality() {
		return compressionQuality;
	}
	
	public void setCompressionQuality(float compressionQuality) {
		if (compressionQuality < .1f || compressionQuality > 1f) {
			throw new IllegalArgumentException();
		}
		this.compressionQuality = compressionQuality;
	}
	
	private float compressionQuality = .8f;

	/**
	 * エクスポートウィザードのプリセットにパーツ不足時の警告色(前景色)を取得する.
	 * @return エクスポートウィザードのプリセットにパーツ不足時の警告色(前景色)
	 */
	public Color getExportPresetWarningsForegroundColor() {
		return exportPresetWarningsForegroundColor;
	}
	
	public void setExportPresetWarningsForegroundColor(
			Color exportPresetWarningsForegroundColor) {
		this.exportPresetWarningsForegroundColor = exportPresetWarningsForegroundColor;
	}
	
	private Color exportPresetWarningsForegroundColor = Color.red;
	
	/**
	 * JARファイル転送用バッファサイズ.<br>
	 * @return JARファイル転送用バッファサイズ.
	 */
	public int getJarTransferBufferSize() {
		return jarTransferBufferSize;
	}
	
	public void setJarTransferBufferSize(int jarTransferBufferSize) {
		if (jarTransferBufferSize <= 0) {
			throw new IllegalArgumentException();
		}
		this.jarTransferBufferSize = jarTransferBufferSize;
	}

	private int jarTransferBufferSize = 4096;
	
	/**
	 * ZIPファイル名のエンコーディング.<br>
	 * @return ZIPファイル名のエンコーディング.<br>
	 */
	public String getZipNameEncoding() {
		return zipNameEncoding;
	}
	
	public void setZipNameEncoding(String zipNameEncoding) {
		if (zipNameEncoding == null) {
			throw new IllegalArgumentException();
		}
		try {
			Charset.forName(zipNameEncoding);
		} catch (Exception ex) {
			throw new RuntimeException("unsupported charset: " + zipNameEncoding);
		}
		this.zipNameEncoding = zipNameEncoding;
	}
	
	private String zipNameEncoding = "csWindows31J";

	/**
	 * ディセーブルなテーブルのセルのフォアグラウンドカラーを取得する.
	 * @return ディセーブルなテーブルのセルのフォアグラウンドカラー
	 */
	public Color getDisabledCellForgroundColor() {
		return disabledCellForegroundColor;
	}
	
	public void setDisabledCellForegroundColor(Color disabledCellForegroundColor) {
		if (disabledCellForegroundColor == null) {
			throw new IllegalArgumentException();
		}
		this.disabledCellForegroundColor = disabledCellForegroundColor;
	}
	
	private Color disabledCellForegroundColor = Color.gray;
	
	
	/**
	 * ディレクトリを監視する間隔(mSec)を取得する.
	 * @return ディレクトリを監視する間隔(mSec)
	 */
	public int getDirWatchInterval() {
		return dirWatchInterval;
	}
	
	public void setDirWatchInterval(int dirWatchInterval) {
		if (dirWatchInterval <= 0) {
			throw new IllegalArgumentException();
		}
		this.dirWatchInterval = dirWatchInterval;
	}
	
	private int dirWatchInterval = 7 * 1000;
	
	/**
	 * ディレクトリの監視を有効にするか?
	 * @return ディレクトリの監視を有効にする場合はtrue
	 */
	public boolean isEnableDirWatch() {
		return enableDirWatch;
	}
	
	public void setEnableDirWatch(boolean enableDirWatch) {
		this.enableDirWatch = enableDirWatch;
	}
	
	private boolean enableDirWatch = true;
	
	/**
	 * ファイル転送に使うバッファサイズ.<br>
	 * @return バッファサイズ
	 */
	public int getFileTransferBufferSize() {
		return fileTransferBufferSize;
	}
	
	public void setFileTransferBufferSize(int fileTransferBufferSize) {
		if (fileTransferBufferSize <= 0) {
			throw new IllegalArgumentException();
		}
		this.fileTransferBufferSize = fileTransferBufferSize;
	}
	
	private int fileTransferBufferSize = 4096;
	
	/**
	 * プレビューのインジケータを表示するまでのディレイ(mSec)を取得する.
	 * @return プレビューのインジケータを表示するまでのディレイ(mSec)
	 */
	public long getPreviewIndicatorDelay() {
		return previewIndeicatorDelay;
	}
	
	public void setPreviewIndeicatorDelay(long previewIndeicatorDelay) {
		if (previewIndeicatorDelay < 0) {
			throw new IllegalArgumentException();
		}
		this.previewIndeicatorDelay = previewIndeicatorDelay;
	}
	
	private long previewIndeicatorDelay = 300;
	
	/**
	 * 情報ダイアログの編集ボタンを「開く」アクションにする場合はtrue、「編集」アクションにする場合はfalse
	 * @return trueならばOpen、falseならばEdit
	 */
	public boolean isInformationDialogOpenMethod() {
		return informationDialogOpenMethod;
	}
	
	public void setInformationDialogOpenMethod(
			boolean informationDialogOpenMethod) {
		this.informationDialogOpenMethod = informationDialogOpenMethod;
	}

	private boolean informationDialogOpenMethod = true;
	
	/**
	 * ログを常に残すか?<br>
	 * falseの場合は{@link ApplicationLogHandler}の実装に従って終了時に
	 * 必要なければログは削除される.<br>
	 * @return 常に残す場合はtrue、そうでなければfalse
	 */
	public boolean isNoRemoveLog() {
		return noRemoveLog;
	}

	public void setNoRemoveLog(boolean noRemoveLog) {
		this.noRemoveLog = noRemoveLog;
	}
	
	private boolean noRemoveLog = false;


	/**
	 * テーブルのグリッド色.<br>
	 * @return テーブルのグリッド色
	 */
	public Color getGridColor() {
		return gridColor;
	}
	
	public void setGridColor(Color gridColor) {
		if (gridColor == null) {
			throw new IllegalArgumentException();
		}
		this.gridColor = gridColor;
	}
	
	private Color gridColor = Color.gray;

	/**
	 * カラーダイアログの値が変更されたら、自動的にプレビューを更新するか?
	 * @return カラーダイアログの値が変更されたら、自動的にプレビューを更新する場合はtrue (デフォルトはtrue)
	 */
	public boolean isEnableAutoColorChange() {
		return enableAutoColorChange;
	}
	
	public void setEnableAutoColorChange(boolean enableAutoColorChange) {
		this.enableAutoColorChange = enableAutoColorChange;
	}
	
	private boolean enableAutoColorChange = true;
	
	public void setAuthorEditConflictBgColor(Color authorEditConflictBgColor) {
		if (authorEditConflictBgColor == null) {
			throw new IllegalArgumentException();
		}
		this.authorEditConflictBgColor = authorEditConflictBgColor;
	}

	/**
	 * パーツの作者編集時に複数作者を選択した場合のに入力ボックスの背景色
	 * @return 背景色
	 */
	public Color getAuthorEditConflictBgColor() {
		return authorEditConflictBgColor;
	}
	
	Color authorEditConflictBgColor = Color.yellow;
	
	
	public void setMainFrameMaxWidth(int width) {
		this.mainFrameMaxWidth = width;
	}
	
	/**
	 * メインフレームの初期表示時の最大幅
	 * @return メインフレームの初期表示時の最大幅
	 */
	public int getMainFrameMaxWidth() {
		return mainFrameMaxWidth;
	}
	
	private int mainFrameMaxWidth = 800;
	
	public void setMainFrameMaxHeight(int height) {
		this.mainFrameMaxHeight = height;
	}

	/**
	 * メインフレームの初期表示時の最大高さ
	 * @return メインフレームの初期表示時の最大高さ
	 */
	public int getMainFrameMaxHeight() {
		return mainFrameMaxHeight;
	}
	
	private int mainFrameMaxHeight = 600;
	
	
	/**
	 * カラーダイアログで存在しないレイヤーをディセーブルにしない.
	 * @return ディセーブルにしない場合はtrue
	 */
	public boolean isNotDisableLayerTab() {
		return notDisableLayerTab;
	}
	
	public void setNotDisableLayerTab(boolean notDisableLayerTab) {
		this.notDisableLayerTab = notDisableLayerTab;
	}
	
	private boolean notDisableLayerTab;
	
}
