package jp.sf.amateras.functions;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Map;

import jp.sf.amateras.functions.utils.DateUtils;
import jp.sf.amateras.functions.utils.NumberUtil;
import jp.sf.amateras.functions.utils.RequestUtils;
import jp.sf.amateras.functions.utils.StringUtils;
import net.arnx.jsonic.JSON;

/**
 * JSPで利用可能な標準的なEL関数を提供します。
 *
 * @author Naoki Takezoe
 */
public class Functions {

	/**
	 * 指定した文字列に含まれるHTMLタグをエスケープします。
	 *
	 * @param input 変換対象のオブジェクト
	 * @return 変換後の文字列。引数<code>value</code>が<code>null</code>の場合は空文字列
	 * @see StringUtils#escapeHtml(String)
	 */
	public static String h(Object input){
        if (input == null) {
            return "";
        }
        String str = "";
        if (input.getClass().isArray()) {
            Class<?> clazz = input.getClass().getComponentType();
            if (clazz == String.class) {
                str = Arrays.toString((Object[]) input);
            } else if (clazz == boolean.class) {
                str = Arrays.toString((boolean[]) input);
            } else if (clazz == char.class) {
            	str = Arrays.toString((char[]) input);
            } else if (clazz == int.class) {
                str = Arrays.toString((int[]) input);
            } else if (clazz == long.class) {
                str = Arrays.toString((long[]) input);
            } else if (clazz == byte.class) {
                str = Arrays.toString((byte[]) input);
            } else if (clazz == short.class) {
                str = Arrays.toString((short[]) input);
            } else if (clazz == float.class) {
                str = Arrays.toString((float[]) input);
            } else if (clazz == double.class) {
                str = Arrays.toString((double[]) input);
            } else {
                str = Arrays.toString((Object[]) input);
            }
        } else {
            str = input.toString();
        }
        return StringUtils.escapeHtml(str);
    }

	/**
	 * 指定した文字列をURLエンコードします。
	 *
	 * @param value 変換対象の文字列
	 * @return 変換後の文字列。引数<code>value</code>が<code>null</code>の場合は空文字列
	 * @see StringUtils#urlEncode(String)
	 */
	public static String u(String value){
		String encoding = RequestUtils.getEncoding();

		if(encoding != null){
			return StringUtils.urlEncode(value, encoding);
		}

		return StringUtils.urlEncode(value);
	}

	/**
	 * 指定した文字列の改行を&lt;br&gt;タグに変換します。
	 *
	 * @param value 変換対象の文字列
	 * @return 変換後の文字列。引数<code>value</code>が<code>null</code>の場合は空文字列
	 * @see StringUtils#convertLineSeparator(String)
	 */
	public static String br(String value){
		return StringUtils.convertLineSeparator(value);
	}

	/**
	 * 指定した文字列に含まれるタブや半角スペースを&amp;nbsp;に変換します。
	 * <p>
	 * 連続した半角スペースの最初の1つめは&amp;nbsp;に変換しません。
	 * また、タブ文字は半角スペース4つ分の空白に変換します。
	 *
	 * @param value 変換対象の文字列
	 * @return 変換後の文字列。引数<code>value</code>が<code>null</code>の場合は空文字列
	 * @see StringUtils#convertWhitespace(String)
	 */
	public static String nbsp(String value){
		return StringUtils.convertWhitespace(value);
	}

	/**
	 * 文字列を*（アスタリスク）に変換します。
	 *
	 * @param value 変換対象の文字列
	 * @return 変換後の文字列。引数<code>value</code>が<code>null</code>の場合は空文字列
	 * @see StringUtils#mask(String, char)
	 */
	public static String mask(String value){
		return StringUtils.mask(value, '*');
	}

	/**
	 * <code>java.util.Date</code>オブジェクトを日付形式にフォーマットします。
	 *
	 * @param date 変換対象の<code>java.util.Date</code>オブジェクト
	 * @return 日付形式にフォーマットされた文字列。引数<code>date</code>が<code>null</code>の場合は空文字列
	 * @see DateUtils#formatDate(Date)
	 */
	public static String date(Date date){
		return DateUtils.formatDate(date);
	}

	/**
	 * <code>java.util.Date</code>オブジェクトを日時形式にフォーマットします。
	 *
	 * @param date 変換対象の<code>java.util.Date</code>オブジェクト
	 * @return 日時形式にフォーマットされた文字列。引数<code>date</code>が<code>null</code>の場合は空文字列
	 * @see DateUtils#formatDatetime(Date)
	 */
	public static String datetime(Date date){
		return DateUtils.formatDatetime(date);
	}

	/**
	 * <code>java.util.Date</code>オブジェクトを時間形式にフォーマットします。
	 *
	 * @param date 変換対象の<code>java.util.Date</code>オブジェクト
	 * @return 時間形式にフォーマットされた文字列。引数<code>date</code>が<code>null</code>の場合は空文字列
	 * @see DateUtils#formatTime(Date)
	 */
	public static String time(Date date){
		return DateUtils.formatTime(date);
	}

	/**
	 * 数値を指定したパターンでフォーマットします。
	 *
	 * @param number フォーマット対象の数値
	 * @param pattern <code>java.text.DecimalFormat</code>で指定可能なパターン
	 * @return フォーマットされた文字列。引数<code>number</code>が<code>null</code>の場合は空文字列
	 * @see NumberUtil#formatNumber(Object, String)
	 */
	public static String number(Object number, String pattern){
		return NumberUtil.formatNumber(number, pattern);
	}

	/**
	 * 文字列の先頭から指定文字数までを切り出します。
	 *
	 * @param value 文字列
	 * @param length 切り出す文字数
	 * @return 切り出された文字列。引数<code>value</code>が<code>null</code>の場合は空文字列
	 * @see StringUtils#cut(String, int)
	 */
	public static String cut(String value, int length){
		return StringUtils.cut(value, length);
	}

	/**
	 * 文字列に含まれるURLをリンクに変換します。
	 *
	 * @param value 変換対象の文字列
	 * @return 変換後の文字列。引数<code>value</code>が<code>null</code>の場合は空文字列
	 * @see StringUtils#convertURL(String)
	 */
	public static String link(String value){
		return StringUtils.convertURL(value);
	}

	/**
	 * オブジェクトをJSON形式の文字列に変換します。
	 * JSONへの変換には<a href="http://jsonic.sourceforge.jp/">JSONIC</a>を使用します。
	 *
	 * @param bean JSONに変換するオブジェクト
	 * @return JSON形式の文字列。
	 *   引数<code>bean</code>が<code>null</code>の場合は<code>&quot;null&quot;</code>を返します。
	 */
	public static String json(Object bean){
		if(bean == null){
			return "null";
		}

		return JSON.encode(bean);
	}

	/**
	 * マップのキーと値からクエリ文字列を組み立てます。
	 *
	 * @param params パラメータ名と値を格納したマップ
	 * @return 引数で渡されたマップから組み立てられたクエリ文字列
	 * @see StringUtils#map2queryString(Map, String)
	 */
	@SuppressWarnings("unchecked")
	public static String query(Object params){
		String encoding = RequestUtils.getEncoding();

		if(params instanceof Map){
			if(encoding != null){
				return StringUtils.map2queryString((Map<String, Object>) params, encoding);
			}
			return StringUtils.map2queryString((Map<String, Object>) params);
		}

		// TODO MapだけじゃなくてJavaBeanのプロパティにも対応する

		return "";
	}

	/**
	 * 文字列が正規表現にマッチするかどうかをテストします。
	 *
	 * @param value テスト対象の文字列
	 * @param pattern 正規表現
	 * @return マッチする場合<code>true</code>、マッチしない場合<code>false</code>
	 *   （引数<code>value</code>が<code>null</code>の場合は<code>false</code>を返します）
	 */
	public static boolean matches(String value, String pattern){
		if(value == null){
			return false;
		}

		return value.matches(pattern);
	}

	/**
	 * 文字列を正規表現で置換します。
	 *
	 * @param value 置換対象の文字列
	 * @param pattern 正規表現
	 * @param replacement 正規表現にマッチした部分を置換する文字列
	 * @return 置換後の文字列（引数<code>value</code>が<code>null</code>の場合は空文字列を返します）
	 */
	public static String replace(String value, String pattern, String replacement){
		if(value == null){
			return "";
		}
		return value.replaceAll(pattern, replacement);
	}

	/**
	 * 引数<code>value</code>をJavaScriptの文字列リテラルに埋め込む場合に必要なエスケープ処理を行います。
	 * このメソッドでエスケープした結果はシングルクォートで囲まれることを想定しています。
	 * <p>
	 * EL関数としての使用例を以下に示します。
	 * <pre>
	 * &lt;script type="text/javascript"&gt;
	 *   var message = '${f:js(bean.property)}';
	 *   ...
	 * &lt;/script&gt;
	 * </pre>
	 *
	 * @param value エスケープ対象の文字列
	 * @return エスケープされた文字列
	 * @see StringUtils#escapeJavaScript(String)
	 */
	public static String js(String value){
		return StringUtils.escapeJavaScript(value);
	}

	/**
	 * 2つのオブジェクトを文字列として結合します。引数で渡したオブジェクトがnullの場合は空文字列として扱います。
	 *
	 * @param value1 結合するオブジェクト
	 * @param value2 結合するオブジェクト
	 * @return 結合した文字列
	 */
	public static String concat(Object value1, Object value2){
		return StringUtils.concat(value1, value2);
	}

	/**
	 * <code>value1</code>の中に<code>value2</code>が含まれているかどうかをテストします。
	 * <code>value1</code>には以下のオブジェクトを指定することができます。
	 * <dl>
	 *   <dt>配列</dt>
	 *   <dd>
	 *     配列の要素に<code>value2</code>が含まれているかどうかをテストします。
	 *   </dd>
	 *   <dt>java.util.Collection</dt>
	 *   <dd>
	 *     コレクションの要素に<code>value2</code>が含まれているかどうかをテストします。
	 *   </dd>
	 *   <dt>java.util.Map</dt>
	 *   <dd>
	 *     マップの値に<code>value2</code>が含まれているかどうかをテストします。
	 *   </dd>
	 *   <dt>その他のオブジェクト</dt>
	 *   <dd>
	 *     文字列表現としての<code>value1</code>に部分文字列<code>value2</code>が含まれているかどうかをテストします。
	 *   </dd>
	 * </dl>
	 *
	 * TODO マップの場合はキーを見たほうがいいかも？
	 *
	 * @param value1 検索対象のオブジェクト
	 * @param value2 検索するオブジェクト
	 * @return <code>value1</code>に<code>value2</code>が含まれている場合<code>true</code>、
	 *   含まれていない場合<code>false</code>
	 */
	@SuppressWarnings("rawtypes")
	public static boolean contains(Object value1, Object value2){
		if(value1 == null){
			return false;
		}
		// コレクションの要素
		if(value1 instanceof Collection){
			Collection collection = Collection.class.cast(value1);
			return collection.contains(value2);
		}
		// 配列
		if(value1.getClass().isArray()){
			for(int i=0; i < Array.getLength(value1); i++){
				Object element = Array.get(value1, i);
				if(element == value2){
					return true;
				}
				if(element != null && element.equals(value2)){
					return true;
				}
			}
			return false;
		}
		// マップの値
		if(value1 instanceof Map){
			Map map = (Map) value1;
			return map.containsValue(value2);
		}

		// 文字列
		if(value2 == null){
			return false;
		}

		String str = value1.toString();
		return str.contains(value2.toString());
	}

}
