package jp.ehobby.util.number;

import java.util.HashMap;




/**
 * 数値を扱うユーティリティクラス.
 *
 * @author kei-n
 */
public final class NumberUtilities {

	/** 16進数の基数(16).		*/
	public static int HEX = 16;

	/** 10進数の基数(10).		*/
	public static int DEC = 10;

	/**  8進数の基数(8).		*/
	public static int OCT = 8;

	/**  2進数の基数(2).		*/
	public static int BIN = 2;


	////////////////////////////////////////////////////////////////////////////
	//
	// 数値変換用定義
	//
	/** 数値リスト.				*/
	private static final String[] NUMBER_LIST = {
		"0123456789ABCDEF",								//$NON-NLS-1$
		"0123456789abcdef",								//$NON-NLS-1$
		"０１２３４５６７８９ＡＢＣＤＥＦ",				//$NON-NLS-1$
		"０１２３４５６７８９ａｂｃｄｅｆ"				//$NON-NLS-1$
	};

	/** 数値MAP.				*/
	private static final HashMap<Character, Integer> NUMBER_MAP = new HashMap<>();

	// 数値定義設定
	static {
		for (String numStr : NUMBER_LIST) {
			for (int num = 0; num < numStr.length(); num++) {
				char c = numStr.charAt(num);
				NUMBER_MAP.put(Character.valueOf(c), Integer.valueOf(num));
			}
		}
	}


	/**
	 * 文字列表現の値を long 型に変換します.
	 * 数値に変換できない場合、NumberFormatException を throw する.
	 *
	 * @param strNum 文字列表現の値
	 * @return 値
	 */
	public static long longValue(final String strNum) {
		NumberAnalyzer analyzer = new NumberAnalyzer(strNum);
		return analyzer.getValue();
	}


	/**
	 * 指定された文字表現の数値を数値に変換します.
	 * 数値に変換できない場合 -1 を返します.
	 *
	 * @param c 文字表現数値
	 * @return  数値
	 */
	public static int charToNumber(final char c) {
		Integer val = NUMBER_MAP.get(Character.valueOf(c));
		if (val != null) {
			return val.intValue();
		}
		return -1;
	}

}


/**
 * 文字列形式の数値解析器.
 *
 * @author kei-n
 */
class NumberAnalyzer {

	/** 空白文字列.						*/
	private static final String SPACE_STRINGS   = " \t_,＿，　";			//$NON-NLS-1$

	/** 数値の末尾に付く付加情報文字.	*/
	private static final String ADDINFO_STRINGS = "ulULhHｕｌＵＬｈＨ";	//$NON-NLS-1$

	/** 解析する文字列.	*/
	private final String numStr;

	/** 解析位置.		*/
	private int pos;

	/** 符号.			*/
	private int neg;

	/** 進数.			*/
	private int radix;


	/**
	 * 数値解析コンストラクタ.
	 *
	 * @param number 解析する文字列形式の数値
	 */
	NumberAnalyzer(final String number) {
		this.pos    = 0;
		this.radix  = NumberUtilities.DEC;
		this.numStr = trimAll(number);
		if (this.numStr.length() == 0) {
			// 空文字、または空白文字だけで構成されている
			throw new NumberFormatException("\"" + number + "\"");			//$NON-NLS-1$//$NON-NLS-2$
		}
	}


	/**
	 * 解析後の値を返します.
	 *
	 * @return 値
	 */
	public long getValue() {

		// 符号解析
		analysisNegative();

		// 進数解析
		analysisAdic();

		// 数値解析
		long value = analysisNumber();

		// 符号を付与
		value *= this.neg;

		return value;
	}


	/**
	 * 符号解析.
	 *
	 * 負の符号の場合、this.neg を -1 に設定し、this.posを1進めます.
	 * 正の符号の場合、this.neg を  1 に設定し、this.posを1進めます.
	 * 符号なしの場合、this.neg を  1 に設定します.
	 */
	private void analysisNegative() {
		char c = this.numStr.charAt(this.pos);
		if ((c == '-') || (c == '－') || (c == '‐')) {
			this.neg = -1;
			this.pos++;
		} else if ((c == '+') || (c == '＋')) {
			this.neg = 1;
			this.pos++;
		} else {
			this.neg = 1;
		}
	}


	/**
	 * 進数解析.
	 */
	private void analysisAdic() {
		if ((this.pos + 1) >= this.numStr.length()) {
			// 残り1文字 => 進数をあらわすもの無しなので10進数とする
			this.radix = NumberUtilities.DEC;
			return;
		}

		analysisAdicHead();		// 先頭文字を確認し、進数を判定
		analysisAdicTail();		// 末尾文字を確認し、進数を判定
	}


	/**
	 * 指定された先頭2文字より、進数を判定します.
	 * 進数判定後、適宜次の読み取り位置を勧めます.
	 */
	private void analysisAdicHead() {
		char c = this.numStr.charAt(this.pos);
		if ((c != '0') && (c != '０')) {			// 先頭0以外の場合は、10進数
			this.radix = NumberUtilities.DEC;
			return;
		}

		this.pos++;
		c = this.numStr.charAt(this.pos);
		switch (c) {
		case 'x': case 'X': case 'ｘ': case 'Ｘ':	// 16進数
			this.radix = NumberUtilities.HEX;
			this.pos++;
			break;
		case 'b': case 'B': case 'ｂ': case 'Ｂ':	//  2進数
			this.radix = NumberUtilities.BIN;
			this.pos++;
			break;
		default:									//  8進数
			this.radix = NumberUtilities.OCT;
		}
	}


	/**
	 * 末尾の文字より、進数を判定します.
	 */
	private void analysisAdicTail() {
		// 最後の文字が h, H か否かを確認
		char c = this.numStr.charAt(this.numStr.length() - 1);
		if ((c == 'h') || (c == 'H') || (c == 'ｈ') || (c == 'Ｈ')) {

			if (this.radix == NumberUtilities.BIN) {
				// 0B000001 H といった場合、Bは数値とみなす必要がある.
				this.pos--;
			}

			// 末尾に h がついているので 16進数
			this.radix = NumberUtilities.HEX;
		}
	}


	/**
	 * 数値を解析し、long型で返します.
	 */
	private long analysisNumber() {
		long value = 0;
		for (;this.pos < this.numStr.length(); this.pos++) {
			char c = this.numStr.charAt(this.pos);
			int tmpVal = NumberUtilities.charToNumber(c);
			if ((tmpVal < 0) || (this.radix <= tmpVal)) {
				if (ADDINFO_STRINGS.indexOf(c) >= 0) {
					break;
				}
				throw new NumberFormatException("invalid character '" + c + "'");	//$NON-NLS-1$//$NON-NLS-2$
			}
			value *= this.radix;
			value += tmpVal;
		}
		return value;
	}


	////////////////////////////////////////////////////////////////////////////
	//
	// 以下、文字操作などのユーティリティメソッド
	//
	/**
	 * 指定された文字列より、空白文字、アンダーバー、カンマなどを
	 * 取り除いた文字列を返します.
	 * @param str 文字列
	 * @return 空白文字、アンダーバー、カンマを取り除いた文字列
	 */
	private static String trimAll(final String str) {
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < str.length(); i++) {
			char c = str.charAt(i);
			if (SPACE_STRINGS.indexOf(c) < 0) {
				sb.append(c);
			}
		}
		return sb.toString();
	}


}