// =============================================================================
//  property
//  Copyright (c) 2005 - 2013  kei-n
//  URL : http://ehobby.jp
// =============================================================================
module dworld.util.property;

private
{
	import std.conv;
	import std.stream;
	import std.string;
	import std.regex;

	import dworld.util.number;
}


/**
 * プロパティエラー.
 */
class PropertyException : Exception
{
	this(string msg, string file = __FILE__, int line = __LINE__, Throwable next = null)
	{
		super(msg, file, line, next);
	}
}



/**
 * キーと値のペアを扱うプロパティクラス.
 */
public
class Property
{
	/** プロパティマップ.	*/
	private string[string] map;


	/**
	 * 指定されたプロパティの要素をもつプロパティを生成します.
	 *
	 * Params:
	 *	prop = プロパティ
	 */
	public
	this(Property prop = null)
	{
		if (prop !is null)
		{
			addProperty(prop);
		}
	}
	unittest
	{
		Property inProp = new Property();
		inProp.setValue("key", "val");
		Property prop   = new Property(inProp);
		assert("key=val," == prop.toString());
	}


	/**
	 * キーに対応する値を取得します.
	 * 対応する値がない場合, 変換に失敗した場合, def を返します.
	 *
	 * Params:
	 *	key = キー
	 *	def = デフォルト値
	 * Return:
	 *	値
	 */
	public
	string getValue(string key, string def = null)
	{
		if (key !in this.map)
		{
			return def;
		}
		return this.map[key];
	}


	/**
	 * キーに対応する値を取得します.
	 * 対応する値がない場合, 変換に失敗した場合, def を返します.
	 *
	 * Params:
	 *	key = キー
	 *	def = デフォルト値
	 * Return:
	 *	値
	 */
	public
	long longValue(string key, long def = 0)
	{
		string strVal = getValue(key);
		if (strVal is null) { return def; }

		return dworld.util.number.longValue(strVal);
	}


	/**
	 * キーに対応する値を設定します.
	 *
	 * Params:
	 *	key = キー
	 *	val = 値
	 */
	public
	void setValue(string key, string val)
	{
		this.map[cast(string) key.dup] = cast(string) val.dup;
	}


	/**
	 * 指定されたプロパティを追加します.
	 *
	 * Params:
	 *	prop = プロパティ
	 */
	public
	void addProperty(Property prop)
	{
		foreach (string key, string value; prop)
		{
			setValue(key, value);
		}
	}
	unittest
	{
		Property prop1 = new Property();
		prop1.setValue("key1", "val1");
		prop1.setValue("key2", "val2");
		Property prop2 = new Property(prop1);
		assert(prop2.getValue("key1", "tmp") == "val1");
	}


	/**
	 * foreach 用.
	 * foreach(string value; properties)
	 */
	public
	int opApply(int delegate(ref string v) dg)
	{	int result = 0;
		foreach (string value; this.map)
		{
			result = dg(value);
			if (result)
			{
				break;
			}
		}
		return result;
	}
	unittest
	{
		Property prop = new Property();
		prop.setValue("key1", "val1");
		prop.setValue("key2", "val2");
		int i = 0;
		foreach (string value; prop) {	i++; }
		assert(i == 2);
	}


	/**
	 * foreach 用
	 * foreach (string key, string value; properties)
	 */
	public
	int opApply(int delegate(ref string k, ref string v) dg)
	{	int result = 0;
		foreach (string key, string value; this.map)
		{
			result = dg(key, value);
			if (result)
			{
				break;
			}
		}
		return result;
	}
	unittest
	{
		Property prop = new Property();
		prop.setValue("key1", "val1");
		prop.setValue("key2", "val2");
		prop.setValue("key3", "val3");
		int i = 0;
		foreach(string key, string value; prop)
		{
			i++;
		}
		assert(i == 3);
	}


	/**
	 * 本クラスの文字列表現を返します.
	 *
	 * Return:
	 *	文字列表現
	 */
	public override
	string toString()
	{
		char[] result;
		foreach (string key, string value; this.map)
		{
			result ~= key ~ "=" ~ value ~ ",";
		}
		return cast(string) result;
	}
}



/**
 * ファイルからプロパティをロードするローダ.
 */
public
class FilePropertyLoader
{

	private
	{
		/** マクロ import.		*/
		static string MACRO_IMPORT = "${import}";

		/** import スタック.	*/
		bool[string] importStack;

		/** ロード先.			*/
		Property loadProp;

	}


	/**
	 * 指定されたプロパティに
	 */
	public
	this(Property prop)
	{
		this.loadProp = prop;
	}
	unittest
	{
		// テスト用ファイル生成
		string data1 = "# ut1\r\n${import} ut2.temp\r\nkey1 = val1";
		string data2 = "# ut2\r\n${import} ut1.temp\r\nkey2 = ${key1}";
		string data3 = "# ut2\r\nkey3";
		scope(exit) {
			remove("ut1.temp");
			remove("ut2.temp");
			remove("ut3.temp");
		}
		write("ut1.temp", data1);
		write("ut2.temp", data2);
		write("ut3.temp", data3);

		// ロードテスト
		Property           prop   = new Property();
		FilePropertyLoader loader = new FilePropertyLoader(prop);
		loader.load("ut2.temp");
		assert(prop.getValue("key2", "") == "val1");

		try {
			loader.load("ut3.temp");
		} catch (PropertyException e) {
			assert(true);
		}
	}



	/**
	 * 指定されたファイルよりプロパティをロードします.
	 *
	 * Params:
	 *  file = 読み出すファイル
	 */
	public
	void load(string file)
	{
		this.importStack = null;
		loadCore(file);
	}


	/**
	 * 指定されたファイルよりプロパティをロードします.
	 *
	 * Params:
	 *  file = 読み出すファイル
	 */
	private
	void loadCore(string file)
	{
		if (file in importStack)
		{	// 二重 import 防止
			return;
		}
		this.importStack[file] = true;

		BufferedFile bf = new BufferedFile(file);
		scope(exit) bf.close();
	
		foreach (ulong n, char[] line; bf)
		{
			analysis(file, n, cast(string) line);
		}

	}


	/**
	 * プロパティファイルの一行を解析します.
	 *
	 * Params:
	 *  line = 行
	 */
	private
	void analysis(string file, ulong n, string line)
	{
		// コメント, 前後の空白を取り除く
		string noCommentLine = strip(removeComment(line));
		if (noCommentLine.length == 0)
		{	// 空行は無視
			return;
		}

		// ${import} 解析
		int check = indexOf(line, MACRO_IMPORT);
		if (check == 0)
		{	// import 文
			analysisImport(file, n, noCommentLine);
			return;
		}

		// key = value 形式解析
		analysisKeyValue(file, n, noCommentLine);
	}


	/**
	 * ${import} filename の形式を解析し, filename をプロパティとして
	 * 読み込みます.
	 *
	 * Params:
	 *  file = 解析中のプロパティファイル
	 *  n    = 行番号
	 *  line = 行
	 */
	private
	void analysisImport(string file, ulong n, string line)
	{
		string fileName = strip(line[MACRO_IMPORT.length .. $]);
		loadCore(fileName);
	}


	/**
	 * key = value 形式の行を解析し, プロパティに設定します.
	 *
	 * Params:
	 *  file = ファイル名
	 *  n    = 行番号
	 *  line = 行文字列(コメント除去済み)
	 */
	private
	void analysisKeyValue(string file, ulong n, string line)
	{
		string temp = line;
		// 多少速度は犠牲になるが, とりあえず分かりやすい方法で実装
		foreach (string key, string value; this.loadProp)
		{
			temp = replace(temp, regex("$${" ~ key ~ "}"), value);
		}
		

		// キー = 値を探す.
		int eqPos = indexOf(temp, '=');
		if (eqPos < 0)
		{
			throw new PropertyException("not found '='", file, cast(int) n);
		}

		string resKey = strip(temp[0 .. eqPos]);
		string resVal = strip(temp[(eqPos + 1) .. $]);
		this.loadProp.setValue(resKey, resVal);
	}


	/**
	 * コメントを取り除いた文字列を返します.
	 *
	 * Params:
	 *  line = 行
	 * Return:
	 *  コメントを取り除いた文字列を返します.
	 */
	private static
	string removeComment(string line)
	{
		// 後ろから # を検索
		int pos = lastIndexOf(line, '#');
		if (pos < 0)
		{	// コメントではない
			return line;
		}

		bool check = isEscape(line, pos);
		if (check)
		{
			return line;
		}
		return line[0 .. pos];
	}
	unittest
	{
		assert(removeComment("")         == "");
		assert(removeComment("#")        == "");
		assert(removeComment("#A")       == "");
		assert(removeComment("A#")       == "A");
		assert(removeComment("A#B")      == "A");
		assert(removeComment("\\#")      == "\\#");
		assert(removeComment("\\#A")     == "\\#A");
		assert(removeComment("\\\\#")    == "\\\\");
		assert(removeComment("\\\\#A")   == "\\\\");
		assert(removeComment("\\\\\\#")  == "\\\\\\#");
		assert(removeComment("\\\\\\#A") == "\\\\\\#A");
	}



	/**
	 * 指定された文字がESCAPE されているかどうか判定します.
	 *
	 * Params:
	 *  line = 文字列
	 *  pos  = 判定する文字位置
	 * Return:
	 *  true/false (ESCAPEされている/されていない)
	 */
	private static
	bool isEscape(string line, in int pos)
	{
		int cnt = 0;
		for (int i = (pos - 1 - cnt); i >= 0; i--)
		{
			if (line[i] != '\\')
			{
				break;
			}
			cnt++;
		}
		return ((cnt & 0x01) == 0x01);
	}
}

