package jp.odakyu.toiletsignage.util;

import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.NumberFormat;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 文字列を扱うための便利機能を提供します。
 * @author macrojin
 * @version 1.0.1
 */
public class StringUtil {
	/**
	 * 空の文字列を表します。 このフィールドは読み取り専用です。<br>
	 * このフィールドの値は、長さ 0 の文字列 "" です。
	 * @since 1.0.0
	 */
	public static final String Empty = "";
	/**
	 * 空白（スペース）文字列を示します。このフィールドは読み取り専用です。
	 * @since 1.0.0
	 */
	public static final String Space = " ";
	/**
	 * キャリッジリターン(CR)文字の\rです。このフィールドは読み取り専用です。
	 * @since 1.0.0
	 */
	public static final String CarriageReturn = "\r";
	/**
	 * ラインフィード(LF)文字の\nです。このフィールドは読み取り専用です。
	 * @since 1.0.0  
	 */
	public static final String LineFeed = "\n";
	/**
	 * CRLFをあわせたものです。
	 * @since 1.0.1
	 */
	public static final String CRLF = CarriageReturn + LineFeed;
	
	/**
	 * スラッシュ文字です。
	 * @since 1.0.0
	 */
	public static final String Slash = "/";
	public static final String BackSlash = "\\";
	public static final String Hyphen = "-";
	public static final String QuestionMark = "?";
	
	private static final String RegexBriceL = "[";
	private static final String RegexBriceR = "]";
	private static final String RegexPlus = "+";
	private static final String RegexAlphabet = "a-zA-Z";
	private static final String RegexNumeric = "0-9";
	private static final String RegexTrimLastSlashOrSpace = "[/ ]+$";
	
	private static String ELLIPSIS = "...";

	/**
	 * 指定された文字列が null または  {@link StringUtil#Empty} 文字列であるかどうかを示します。
	 * @param s テストする文字列。
	 * @return s パラメーターが null または空の文字列 ("") の場合は true。それ以外の場合は false。
	 * @since 1.0.0
	 */
	public static boolean isNullOrEmpty(String s) {
		return s == null || s.equals(Empty);
	}
	
	/**
	 * 指定された文字列が null または  {@link StringUtil#Empty} 文字列であるかどうかを示します。
	 * @param s テストする文字列。
	 * @return パラメーターが null または空の文字列 ("") の場合は true。それ以外の場合は false。
	 * @since 1.0.0
	 */
	public static boolean isNullOrEmpty(StringBuffer s) {
		return s == null || isNullOrEmpty(s.toString());
	}
	
	/**
	 * 指定された文字列が null または  {@link StringUtil#Empty} 文字列であるかどうかを示します。
	 * @param s テストする文字列。
	 * @return パラメーターが null または空の文字列 ("") の場合は true。それ以外の場合は false。
	 * @since 1.0.0
	 */
	public static boolean isNullOrEmpty(StringBuilder s) {
		return s == null || isNullOrEmpty(s.toString());
	}
	
	/**
	 * 指定された文字列が null または空であるか、空白文字だけで構成されているかどうかを示します。
	 * @param s テストする文字列。
	 * @return s パラメーターが null または {@link StringUtil#Empty} であるか、s が空白文字だけで構成されている場合は true。
	 * @since 1.0.0
	 */
	public static boolean isNullOrWhiteSpace(String s) {
		return isNullOrEmpty(s) || s.trim().length() == 0;
	}
	
	/**
	 * 指定された文字列が null または空であるか、空白文字だけで構成されているかどうかを示します。
	 * @param s テストする文字列。
	 * @return s パラメーターが null または {@link StringUtil#Empty} であるか、s が空白文字だけで構成されている場合は true。
	 * @since 1.0.0
	 */
	public static boolean isNullOrWhiteSpace(StringBuffer s) {
		return isNullOrEmpty(s) || s.toString().trim().length() == 0;
	}
	
	/**
	 * 指定された文字列が null または空であるか、空白文字だけで構成されているかどうかを示します。
	 * @param s テストする文字列。
	 * @return s パラメーターが null または {@link StringUtil#Empty} であるか、s が空白文字だけで構成されている場合は true。
	 * @since 1.0.0
	 */
	public static boolean isNullOrWhiteSpace(StringBuilder s) {
		return isNullOrEmpty(s) || s.toString().trim().length() == 0;
	}
	
	/**
	 * オブジェクトを文字列に変換します。変換には　{@link Object#toString()}　を利用します。
	 * @param obj 変換するオブジェクトです。
	 * @return 変換した文字列を返します。変換できない場合、又は変換した文字列が空文字である場合、{@link StringUtil#Empty} を返します。。
	 * @since 
	 */
	public static String toString(Object obj) {
		return toString(obj, Empty);
	}
	
	/**
	 * オブジェクトを文字列に変換します。変換には　{@link Object#toString()}　を利用します。
	 * @param obj 変換するオブジェクトです。
	 * @param defaultValue　変換できない場合、又は変換した文字列が空文字である場合、使用するデフォルト値です。
	 * @return 変換した文字列を返します。変換できない場合、又は変換した文字列が空文字である場合、defaultValueを返します。
	 * @since 
	 */
	public static String toString(Object obj, String defaultValue) {
		if (obj != null && !isNullOrEmpty(obj.toString())) { // Comment: toStringを2度呼んでいるためパフォーマンスが低下
			return obj.toString();
		} else {
			return defaultValue;
		}
	}
	
	/**
	 * 指定された文字列を空にします。
	 * @param s 空にする文字列
	 * @since 1.0.0
	 */
	public static void clear(StringBuffer s) {
		if (!isNullOrEmpty(s) && s.length() > 0) {
			s.delete(0, s.length());
		}
	}
	
	/**
	 * 指定された文字列を空にします。
	 * @param s 空にする文字列
	 * @since 1.0.0
	 */
	public static void clear(StringBuilder s) {
		if (!isNullOrEmpty(s) && s.length() > 0) {
			s.delete(0, s.length());
		}
	}
	
	/**
	 * 文字列の最後を改行します。
	 * @param s 改行する文字列
	 * @since 1.0.0
	 */
	public static void appendLine(StringBuffer s) {
		s.append(StringUtil.LineFeed);
	}
	
	/**
	 * 文字列の最後を改行します。
	 * @param s 改行する文字列
	 * @since 1.0.0 
	 */
	public static void appendLine(StringBuilder s) {
		s.append(StringUtil.LineFeed);
	}
	
	/**
	 * 文字列 s に複数の文字列を追加し、改行します。
	 * @param s 改行する文字列
	 * @param arg 追加する文字列
	 * @since 1.0.0
	 */
	public static void appendLine(StringBuffer s, String arg) {
		s.append(arg);
		appendLine(s);
	}
	
	/**
	 * 文字列 s に複数の文字列を追加し、改行します。
	 * @param s 改行する文字列
	 * @param arg 追加する文字列
	 * @since 1.0.0
	 */
	public static void appendLine(StringBuilder s, String arg) {
		s.append(arg);
		appendLine(s);
	}
	
	/**
	 * 文字列 s に複数の文字列を追加し、改行します。
	 * @param s 改行する文字列
	 * @param args 追加する文字列
	 * @since 1.0.0
	 */
	public static void appendMultiLine(StringBuffer s, String...args) {
		for (String string : args) {
			appendLine(s, string);
		}
	}
	
	/**
	 * 文字列 s に複数の文字列を追加し、改行します。
	 * @param s 改行する文字列
	 * @param args 追加する文字列
	 * 
	 * @since 1.0.0
	 */
	public static void appendMultiLine(StringBuilder s, String...args) {
		for (String string : args) {
			appendLine(s, string);
		}
	}
	
	/**
	 * 文字列 s に複数の文字列を追加し、改行します。
	 * @param s 改行する文字列
	 * @param arg 追加する文字列
	 * @since 1.0.0
	 */
	public static void appendLine(StringBuffer s, int arg) {
		s.append(arg);
		appendLine(s);
	}
	
	/**
	 * 文字列 s に複数の文字列を追加し、改行します。
	 * @param s 改行する文字列
	 * @param arg 追加する文字列
	 * @since 1.0.0
	 */
	public static void appendLine(StringBuilder s, int arg) {
		s.append(arg);
		appendLine(s);
	}
	
	/**
	 * 文字列 s に複数の文字列を追加し、改行します。
	 * @param s 改行する文字列
	 * @param arg 追加する文字列
	 * @since 1.0.0
	 */
	public static void appendLine(StringBuffer s, long arg) {
		s.append(arg);
		appendLine(s);
	}
	
	/**
	 * 文字列 s に複数の文字列を追加し、改行します。
	 * @param s 改行する文字列
	 * @param arg 追加する文字列
	 * @since 1.0.0
	 */
	public static void appendLine(StringBuilder s, long arg) {
		s.append(arg);
		appendLine(s);
	}
	
	/**
	 * 文字列 s に複数の文字列を追加し、改行します。
	 * @param s 改行する文字列
	 * @param arg 追加する文字列
	 * @since 1.0.0
	 */
	public static void appendLine(StringBuffer s, Object arg) {
		s.append(arg);
		appendLine(s);
	}
	
	/**
	 * 文字列 s に複数の文字列を追加し、改行します。
	 * @param s 改行する文字列
	 * @param arg 追加する文字列
	 * @since 1.0.0
	 */
	public static void appendLine(StringBuilder s, Object arg) {
		s.append(arg);
		appendLine(s);
	}
	
	/**
	 * 指定した文字列を指定した区切りで分割します。分割時にtrimを行います。<br>
	 * 空白の要素は戻り値から省かれます。
	 * @param source 分割する文字列です。
	 * @param delimiter 分割の区切りです。
	 * @return 分割された文字列の配列を返します。
	 * @since 1.0.0
	 */
	public static String[] split(String source, String delimiter) {
		return split(source, delimiter, true);
	}
	
	/**
	 * 指定した文字列を指定した区切りで分割します。
	 * @param source 分割する文字列です。
	 * @param delimiter 分割の区切りです。
	 * @param ignoreEmpty trueにすると、空白の要素は戻り値から省かれます。
	 * @return 分割された文字列の配列を返します。
	 * @since 1.0.0
	 */
	public static String[] split(String source, String delimiter, boolean ignoreEmpty) {
		String[] result = new String[]{source};
		
		if (!isNullOrWhiteSpace(source)) {
			String pattern;
			
			if (ignoreEmpty) {
				pattern = delimiter + "\\s+|" + delimiter;
			} else {
				pattern = delimiter + "\\s|" + delimiter;
			}
			
			result = source.split(pattern);
		}
		
		return result;
	}
	
	/**
	 * 指定した文字列が正しいURLであるかどうかを示します。
	 * @param url テストするURL文字列です。
	 * @return 文字列が正常なURLを示すとtrueを返します。
	 * @since 1.0.0
	 */
	public static boolean validateUrlString(String url) {
		boolean result = true;
		
		try {
            //noinspection ResultOfObjectAllocationIgnored
            new URL(url);
		} catch (Exception e) {
			result = false;
		}
		
		return result;
	}
	
	/**
	 * 指定したURL文字列の後端に/が付いていなければ、/を付けます。<br>
	 * この際、文字列が正しいURLであるかどうかを確認し、有効なURLではない場合、nullを返します。
	 * @param url URL文字列です。
	 * @return　後端に/を付けたURL文字列です。
	 * @since 1.0.0
	 */
	public static String addSlashToUrlString(String url) {
		String result = null;
		
		if (validateUrlString(url)) {
			String temp = url.trim();
			
			if (!temp.endsWith("/")) {
				temp += "/";
			}
			
			result = temp;
		}
		
		return result;
	}
	
	/**
	 * 指定した文字列が半角文字だけで構成されているかどうかを示します。
	 * @param s テストする文字列です。
	 * @param allowAlphabet trueにすると半角英字を許容します。
	 * @param allowNumeric trueにすると半角数字を許容します。
	 * @param allowSymbols 英数字以外に許容したい記号を指定します。たとえば、文字列にスペースを許容したい場合、ここにスペースを指定します。この記号が半角かどうかは判別しません。
	 * @return sが指定した条件に合致するとtrueを返します。
	 * @since 1.0.0
	 */
	public static boolean isHankaku(String s, boolean allowAlphabet, boolean allowNumeric, String...allowSymbols) {
		boolean result = false;
		StringBuffer pattern = new StringBuffer();
		
		if (!isNullOrEmpty(s)
				&& (allowAlphabet
						|| allowNumeric
						|| (allowSymbols != null && allowSymbols.length > 0)
					)
				) {
			pattern.append(RegexBriceL);
			
			if (allowAlphabet) {
				pattern.append(RegexAlphabet);
			}
			
			if (allowNumeric) {
				pattern.append(RegexNumeric);
			}
			
			if (allowSymbols != null && allowSymbols.length > 0) {
				for (String symbol : allowSymbols) {
					if (symbol.equals(Hyphen) || symbol.equals(BackSlash)) {
						pattern.append(BackSlash);
						pattern.append(symbol);
					} else {
						pattern.append(symbol);
					}
				}
			}
			
			pattern.append(RegexBriceR);
			pattern.append(RegexPlus);
			
			result = s.matches(pattern.toString());
		}
		
		return result;
	}
	
	/**
	 * 指定した文字列の最後についている/又は空白を除去します。/又は空白が複数あっても最後であれば全て除去します。
	 * @param s 最後の/又は空白を除去する文字列です。
	 * @return　最後の/又は空白を除去した文字列を返します。
	 * @since 1.0.0
	 */
	public static String trimLastSlashOrSpace(String s) {
		String result = null;
		
		if (!StringUtil.isNullOrEmpty(s)) {
			result = s.replaceAll(RegexTrimLastSlashOrSpace, Empty);
		}
		
		return result;
	}
	
	/**
	 * 指定した複数の文字列sをdelimiterを区切りとして結合します。
	 * @param delimiter 文字列と文字列の区切りです。（カンマ、コロン、空白等）
	 * @param s 結合する文字列の配列です。
	 * @return 結合した文字列を返します。
	 * @since 1.0.0
	 */
	public static String join(String delimiter, String...s) {
		StringBuffer sb = new StringBuffer();
		
		if (s != null && s.length > 0) {
			if (delimiter == null) {
				delimiter = Empty;
			}
			
			for (int i = 0; i < s.length; i++) {
				sb.append(s[i]);
				
				if (i < s.length - 1) {
					sb.append(delimiter);
				}
			}
		}
		
		return sb.toString();
	}

	public static String join(String delimiter, List<?> list) {
		StringBuffer sb = new StringBuffer();
		
		if (list != null && !list.isEmpty()) {
			if (delimiter == null) {
				delimiter = Empty;
			}
			
			for (int i = 0; i < list.size(); i++) {
				sb.append(list.get(i));
				
				if (i < list.size() - 1) {
					sb.append(delimiter);
				}
			}
		}
		
		return sb.toString();
	}

	/**
	 * 数字を3桁ごとのカンマ区切りの文字列に変換して返却します。
	 * 
	 * @param num
	 * @return
	 */
	public static String numberDispFormatComma(Integer num) {
		if (num == null) {
			return "";
		}
		NumberFormat numberFormat = NumberFormat.getNumberInstance();
		return  numberFormat.format(num);
	}

	/**
	 * 指定配列に指定文字列が含まれるかどうかを返す
	 * 
	 * @param search
	 * @param array
	 * @return
	 */
	public static boolean contains(String search, String[] array) {
		if (search == null || array == null || array.length == 0) {
			return false;
		}
		
		for (String string : array) {
			if (string != null && string.equals(search)) {
				return true;
			}
		}
		
		return false;
	}
	
	public static String urlEncode(String s, String enc) throws Exception {
		if (s == null) {
			return null;
		}
		try {
			return URLEncoder.encode(s, enc).replace("+", "%20");
		} catch (UnsupportedEncodingException e) {
			throw new Exception(e);
		}
	}

	public static boolean isNumber(String s) {
		try {
			Long.parseLong(s);
			return true;
		} catch (Exception e) {
			return false;
		}
	}

	public static boolean isReal(String s) {
		try {
			Double.parseDouble(s);
			return true;
		} catch (Exception e) {
			return false;
		}
	}

	public static String chomp(String str) {
		if (str != null && (str.endsWith("\n") || str.endsWith("\r"))) {
			str = str.substring(0, str.length() - 1);
		}
		return str;
	}
	
	/**
	 * 正規表現にしたがって文字列を抽出する
	 *
	 * @param src　対象文字列
	 * @param regex　正規表現
	 * @param index　正規表現中の抽出対象となる()の番号
	 * @return　ヒットした最初の文字列
	 */
	public static String extractRegexString(String src, String regex, int index) {
		Pattern regx = Pattern.compile(regex);
		Matcher m = regx.matcher(src);
		if (m.find()) {
			return m.group(index);
		}
		return null;
	}

	/**
	 * 正規表現にしたがって文字列（複数）を抽出する
	 *
	 * @param src　対象文字列
	 * @param regex　正規表現
	 * @param index　正規表現中の抽出対象となる()の番号（配列）
	 * @return　ヒットしたすべての文字列リスト
	 */
	public static List<List<String>> extractRegexString(String src, String regex, int[] index) {
		Pattern regx = Pattern.compile(regex);
		List<List<String>> list = new LinkedList<List<String>>();
		Matcher m = regx.matcher(src);
		while (m.find()) {
			List<String> matchList = new LinkedList<String>();
			for (int element : index) {
				matchList.add(m.group(element));
			}
			list.add(matchList);
		}
		return list;
	}


	public static boolean equals(String s1, String s2) {
		if (s1 == null) {
			return s2 == null;
		}
		else {
			return s1.equals(s2);
		}
	}

	/**
	 * 第一引数に、第二引数以降のいずれかが一致した場合trueを返す
	 *
	 * @param s
	 * @param targets
	 * @return
	 */
	public static boolean equalsAny(String s, String... targets) {
		for (String target : targets) {
			if (equals(s, target)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 指定文字列に、第二引数以降の文字列のいずれかで終わっているか
	 * 
	 * @param target
	 * @param anys
	 * @return
	 */
	public static boolean endsWithAny(String target, String ...anys) {
		if (target == null || anys == null || anys.length == 0) {
			return false;
		}
		for (String any : anys) {
			if (target.endsWith(any)) {
				return true;
			}
		}
		return false;
	}
	

	/**
	 * 文字列長が指定したmaxlengthを越えないように適宜省略する
	 * @param str 表示される文字列
	 * @param maxlength 許容される最大長
	 * @return
	 */
	public static String abbreviate(String str, int maxlength) {
		/* パラメータ補正 */
		if (str == null) {
            str = "";
        }
		if (maxlength < ELLIPSIS.length()) {
			/* ワーストケースでは ... となるため最低3文字必要 */
			throw new IllegalArgumentException("maxlength is too small");
		}

		final int len = str.length();
		if (len <= maxlength) {
			return	str;
		} else if (len > ELLIPSIS.length()) {
			return	str.substring(0, maxlength - ELLIPSIS.length()) + ELLIPSIS;
		} else {
			return	ELLIPSIS;
		}
	}

	public static boolean startsWith(String str, String target) {
		if (isNullOrEmpty(str) || isNullOrEmpty(target)) {
			return false;
		}
		return str.startsWith(target);
	}

}
