package jp.sf.amateras.functions.utils;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * 文字列関係のユーティリティメソッドを提供します。
 *
 * @author Naoki Takezoe
 */
public class StringUtils {

	private static final int HIGHEST_SPECIAL = '>';

	private static String BR = "<br />";

	private static String NBSP = "&nbsp;";

	private static char[][] specialCharactersRepresentation = new char[HIGHEST_SPECIAL + 1][];

	static {
		specialCharactersRepresentation['&'] = "&amp;".toCharArray();
		specialCharactersRepresentation['<'] = "&lt;".toCharArray();
		specialCharactersRepresentation['>'] = "&gt;".toCharArray();
		specialCharactersRepresentation['"'] = "&#034;".toCharArray();
		specialCharactersRepresentation['\''] = "&#039;".toCharArray();
	}

	private static String defaultEncoding = "UTF-8";

	public static void setDefaultEncoding(String defaultEncoding){
		if(defaultEncoding != null && defaultEncoding.length() != 0){
			StringUtils.defaultEncoding = defaultEncoding;
		}
	}

	/**
	 * 引数で指定した文字列のHTMLタグをエスケープします。
	 *
	 * @param buffer 変換対象の文字列
	 * @return 変換後の文字列。引数<code>value</code>が<code>null</code>の場合は空文字列
	 */
	public static String escapeHtml(String buffer){
		if(buffer == null){
			return "";
		}

        int start = 0;
        int length = buffer.length();
        char[] arrayBuffer = buffer.toCharArray();
        StringBuilder escapedBuffer = null;

        for (int i = 0; i < length; i++) {
            char c = arrayBuffer[i];
            if (c <= HIGHEST_SPECIAL) {
                char[] escaped = specialCharactersRepresentation[c];
                if (escaped != null) {
                    if (start == 0) {
                        escapedBuffer = new StringBuilder(length + 5);
                    }
                    if (start < i) {
                        escapedBuffer.append(arrayBuffer, start, i - start);
                    }
                    start = i + 1;
                    escapedBuffer.append(escaped);
                }
            }
        }
        if (start == 0) {
            return buffer;
        }
        if (start < length) {
            escapedBuffer.append(arrayBuffer, start, length - start);
        }
        return escapedBuffer.toString();
    }

	/**
	 * デフォルトのエンコーディングでURLエンコードを行います。
	 * <p>
	 * デフォルトの文字コードはUTF-8ですが、{@link #setDefaultEncoding(String)}で設定することもできます。
	 *
	 * @param value URLエンコードする文字列
	 * @return URLエンコードされた文字列。引数<code>value</code>が<code>null</code>の場合は空文字列
	 */
	public static String urlEncode(String value){
		return urlEncode(value, defaultEncoding);
	}

	/**
	 * 文字コードを指定してURLエンコードを行います。
	 *
	 * @param value URLエンコードする文字列
	 * @param encode 文字コード
	 * @return URLエンコードされた文字列。引数<code>value</code>が<code>null</code>の場合は空文字列
	 */
	public static String urlEncode(String value, String encode){
		if(value == null){
			return "";
		}
		try {
			return URLEncoder.encode(value, encode);

		} catch (UnsupportedEncodingException ex) {
			throw new RuntimeException(ex);
		}
	}

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

		value = value.replace("\r\n", BR);
		value = value.replace("\r", BR);
		value = value.replace("\n", BR);

		return value;
	}

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

		// タブは半角スペース4つ分に変換
		value = value.replace("\t", "    ");

		StringBuilder sb = new StringBuilder();
		boolean flag = true;

		for(int i=0;i<value.length();i++){
			char c = value.charAt(i);
			if(c == ' '){
				if(flag){
					// 連続した半角スペースの最初の1文字は変換しない
					sb.append(c);
					flag = false;
				} else {
					sb.append(NBSP);
				}
			} else {
				sb.append(c);
				flag = true;
			}
		}

		return sb.toString();
	}

	/**
	 * 引数<code>value</code>と同じ長さの<code>maskChar</code>からなる文字列を返します。
	 * <p>
	 * たとえば、このメソッドを以下のように呼び出した場合、
	 * <code>result</code>は<code>&quot;*******&quot;</code>となります。
	 * <pre> String result = StringUtils.mask("password", '*'); </pre>
	 *
	 * @param value マスク対象の文字列
	 * @param maskChar マスクする文字
	 * @return 引数<code>maskChar</code>でマスクされた文字列。引数<code>value</code>が<code>null</code>の場合は空文字列
	 */
	public static String mask(String value, char maskChar){
		if(value == null){
			return "";
		}
		StringBuilder sb = new StringBuilder();
		for(int i=0; i < value.length(); i++){
			sb.append(maskChar);
		}
		return sb.toString();
	}

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

		StringBuilder sb = new StringBuilder();
		for(int i=0;i < value.length();i++){
			if(i >= length){
				break;
			}
			sb.append(value.charAt(i));
		}

		return sb.toString();
	}

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

		return value.replaceAll(
				"(http|https)://[A-Za-z0-9\\._~/:\\-?&=%;]+", "<a href=\"$0\">$0</a>");
	}
	
	/**
	 * マップに格納されたパラメータからクエリ文字列を組み立てます。
	 * パラメータはデフォルトのエンコーディングでURLエンコードされます。
	 * 
	 * @param map パラメータを格納したマップ
	 * @return クエリ文字列
	 */
	public static String map2queryString(Map<String, Object> map){
		return map2queryString(map, defaultEncoding);
	}
	
	/**
	 * マップに格納されたパラメータからクエリ文字列を組み立てます。
	 * パラメータは引数で指定したエンコーディングでURLエンコードされます。
	 * 
	 * @param map パラメータを格納したマップ
	 * @param encoding URLエンコードのエンコーディング
	 * @return クエリ文字列
	 */
	@SuppressWarnings("unchecked")
	public static String map2queryString(Map<String, Object> map, String encoding){
		if(map == null || map.isEmpty()){
			return "";
		}
		
		StringBuilder sb = new StringBuilder();
		
		for(Entry<String, Object> entry: map.entrySet()){
			String key = entry.getKey();
			Object value = entry.getValue();
			
			if(value instanceof List){
				for(Object item: (List<?>) value){
					appendQueryParameter(sb, key, item, encoding);
				}
			} else if(value.getClass().isArray()){
				for(Object item: (Object[]) value){
					appendQueryParameter(sb, key, item, encoding);
				}
			} else {
				appendQueryParameter(sb, key, value, encoding);
			}
		}
		
		return sb.toString();
	}
	
	private static void appendQueryParameter(StringBuilder sb, 
			String key, Object value, String encoding){
		if(sb.length() != 0){
			sb.append("&");
		}
		// TODO キーもURLエンコードする？
		sb.append(key);
		sb.append("=");
		sb.append(urlEncode(value.toString(), encoding));
	}
	
	/**
	 * 引数<code>value</code>をJavaScriptの文字列リテラルに埋め込む場合に必要なエスケープ処理を行います。
	 * このメソッドでエスケープした結果はシングルクォートで囲まれることを想定しています。
	 * 
	 * @param value エスケープ対象の文字列
	 * @return エスケープされた文字列
	 */
	public static String escapeJavaScript(String value){
		if(value == null){
			return "";
		}
		
		StringBuilder ap = new StringBuilder();
		for (int i = 0; i < value.length(); i++) {
			char c = value.charAt(i);
			switch (c) {
			case '\'':
			case '\\': 
				ap.append('\\').append(c);
				break;
			case '\b':
				ap.append("\\b");
				break;
			case '\f':
				ap.append("\\f");
				break;
			case '\n':
				ap.append("\\n");
				break;
			case '\r':
				ap.append("\\r");
				break;
			case '\t':
				ap.append("\\t");
				break;
			default: 
				ap.append(c);
			}
		}
		return ap.toString();
	}
	
	/**
	 * 2つのオブジェクトを文字列として結合します。引数で渡したオブジェクトがnullの場合は空文字列として扱います。
	 * 
	 * @param value1 結合するオブジェクト
	 * @param value2 結合するオブジェクト
	 * @return 結合した文字列
	 */
	public static String concat(Object value1, Object value2){
		if(value1 == null){
			value1 = "";
		}
		if(value2 == null){
			value2 = "";
		}
		return value1.toString() + value2.toString();
	}
	
}
