package online.view;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Objects;
import java.util.regex.Pattern;

import javax.servlet.ServletResponse;

import core.util.ArrayUtil;
import core.util.MojiUtil;
import core.util.NumberUtil;

/**
 * 表示用ユーティリティ
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class ViewUtil {
	/** 汎用データマップ アトリビュート名 */
	public static final String ATTR_MAP = "vm";

	/** 数値文字参照10進 */
	private static final Pattern PATTERN_DEC = Pattern.compile("^&#[0-9]{2,5};");
	/** 数値文字参照16進 */
	private static final Pattern PATTERN_HEX = Pattern.compile("^&#x[0-9A-Fa-f]{4};");

	/** 数値文字参照サニタイズ */
	private static boolean sanitize = true;

	/**
	 * コンストラクタ
	 *
	 */
	private ViewUtil() {
		throw new AssertionError();
	}

	/**
	 * 数値文字参照サニタイズ設定
	 * @param val 数値文字参照サニタイズ
	 */
	public static void setReferencesSanitize(final boolean val) {
		sanitize = val;
	}

	/**
	 * マッピング取得
	 *
	 * @param response レスポンス
	 * @return マッピング
	 */
	public static String getMapping(final ServletResponse response) {
		String keyword = "charset=";

		String str = response.getContentType();
		if (str != null) {
			int loc = str.indexOf(keyword);
			if (0 <= loc) {
				return str.substring(loc + keyword.length());
			}
		}
		return null;
	}

	/**
	 * マッピング取得
	 *
	 * @param response レスポンス
	 * @return マッピング
	 */
	public static Charset getCharset(final ServletResponse response) {
		String mpg = getMapping(response);
		return mpg != null ? Charset.forName(mpg) : null;
	}

	/**
	 * 文字列を無害化する。
	 *
	 * @param str 変換前文字列
	 * @param mapping マッピング
	 * @param xml XMLタイプの場合 true
	 * @return 変換後文字列
	 */
	public static String sanitize(final String str, final Charset mapping, final boolean xml) {
		StringBuilder sb = null;
		if (mapping != null || xml) {
			// 文字列置換処理
			for (int i = 0; str != null && i < str.length(); i = str.offsetByCodePoints(i, 1)) {
				String after = transfer(str, i, xml, sanitize, false);
				if (after == null && mapping != null) {
					int a = str.codePointAt(i);
					int b = MojiUtil.correctGarbled(a, mapping);
					if (a != b) {
						after = String.valueOf(Character.toChars(b));
					}
				}

				if (after != null) {
					if (sb == null) {
						sb = new StringBuilder(str.substring(0, i));
					}
					sb.append(after);
				} else {
					if (sb != null) {
						sb.appendCodePoint(str.codePointAt(i));
					}
				}
			}
		}
		return Objects.toString(sb, str);
	}

	/**
	 * 変換後文字列取得
	 *
	 * @param str 変換元文字列
	 * @param i 処理対象位置
	 * @param xml XMLタイプの場合 true
	 * @param ref 数値文字参照サニタイズ
	 * @param full 全角化サニタイズ
	 * @return 変換後文字列
	 */
	private static String transfer(final String str, final int i,
					final boolean xml, final boolean ref, final boolean full) {

		int c = str.codePointAt(i);
		String after = null;
		if (xml) {
			if (c == '&') {
				if (!ref && isNumericCharacterReferences(str, i)) {
					after = "&";
				} else {
					after = "&amp;";
				}
			} else if (c == '<') {
				after = "&lt;";
			} else if (c == '>') {
				after = "&gt;";
			} else if (c == '"') {
				after = "&quot;";
			} else if (c == '\'') {
				after = "&apos;";
			}

			if (after == null && full) {
				if (c == '(') {
					after = "（";
				} else if (c == ')') {
					after = "）";
				} else if (c == '{') {
					after = "｛";
				} else if (c == '}') {
					after = "｝";
				} else if (c == '[') {
					after = "［";
				} else if (c == ']') {
					after = "］";
				}
			}
		}

		return after;
	}

	/**
	 * 数値文字参照確認
	 *
	 * @param val 確認対象
	 * @param loc 開始位置
	 * @return 数値文字参照の場合 true を返す
	 */
	private static boolean isNumericCharacterReferences(final String val, final int loc) {
		String str = val.substring(loc, Math.min(val.length(), loc + 10));
		return PATTERN_DEC.matcher(str).matches() || PATTERN_HEX.matcher(str).matches();
	}

	/**
	 * 有害化
	 *
	 * @param str 変更前文字列
	 * @param mapping マッピング
	 * @return 変更後文字列
	 */
	public static String taint(final String str, final Charset mapping) {
		StringBuilder sb = null;
		int i = 0;
		while (str != null && i < str.length()) {
			String after = null;
			int ahead = str.offsetByCodePoints(i, 1);
			if (str.startsWith("&lt;", i)) {
				after = "<";
				ahead = i + "&lt;".length();
			} else if (str.startsWith("&gt;", i)) {
				after = ">";
				ahead = i + "&gt;".length();
			} else if (str.startsWith("&amp;", i)) {
				after = "&";
				ahead = i + "&amp;".length();
			} else if (str.startsWith("&#34;", i)) {
				after = "\"";
				ahead = i + "&#34;".length();
			} else if (str.startsWith("&#39;", i)) {
				// XML
				after = "'";
				ahead = i + "&#39;".length();
			} else if (str.startsWith("&quot;", i)) {
				// XML
				after = "\"";
				ahead = i + "&quot;".length();
			} else if (str.startsWith("&apos;", i)) {
				// XML
				after = "'";
				ahead = i + "&apos;".length();
			} else if (mapping != null) {
				int a = str.codePointAt(i);
				int b = MojiUtil.correctGarbled(a, mapping);
				if (a != b) {
					after = String.valueOf(Character.toChars(b));
				}
			}

			if (after != null) {
				if (sb == null) {
					sb = new StringBuilder(str.substring(0, i));
				}
				sb.append(after);
			} else {
				if (sb != null) {
					sb.appendCodePoint(str.codePointAt(i));
				}
			}

			i = ahead;
		}
		return Objects.toString(sb, str);
	}

	/**
	 * エンコード
	 *
	 * @param str 文字列
	 * @param enc 指定エンコード
	 * @return エンコード文字列
	 */
	public static String encode(final String str, final String enc) {
		try {
			return URLEncoder.encode(
					MojiUtil.correctGarbled(str, Charset.forName(enc)), enc).replace("+", "%20");
		} catch (final UnsupportedEncodingException ex) {
			throw new IllegalArgumentException(enc, ex);
		}
	}

	/**
	 * readonly追加
	 * @param val 文字列
	 * @return 追加後文字列
	 */
	public static String readonly(final Object val) {
		return Objects.toString(val, "") + "\" readonly=\"readonly";
	}

	/**
	 * disabled追加
	 * @param val 文字列
	 * @return 追加後文字列
	 */
	public static String disabled(final Object val) {
		return Objects.toString(val, "") + "\" disabled=\"disabled";
	}

	/**
	 * 小数点切り捨て
	 * @param val 値
	 * @return 切り捨て後文字列
	 */
	public static Integer toInt(final Object val) {
		return NumberUtil.toInteger(Objects.toString(val, null));
	}

	/**
	 * 包含確認
	 * @param col コレクションまたは文字列
	 * @param val 文字列
	 * @return 包含する場合 true を返す。
	 */
	public static Boolean contains(final Object col, final Object val) {
		if (col != null && val != null) {
			if (Collection.class.isInstance(col)) {
				return Boolean.valueOf(Collection.class.cast(col).contains(val));
			} else if (col.getClass().isArray()) {
				return Boolean.valueOf(ArrayUtil.exists(val, Object[].class.cast(col)));
			} else if (String.class.isInstance(col)) {
				return Boolean.valueOf(String.class.cast(col).contains(val.toString()));
			}
		}
		return Boolean.FALSE;
	}

	/**
	 * サニタイズ処理
	 * @param obj 対象オブジェクト
	 * @return サニタイズ文字列
	 */
	public static String escape(final Object obj) {
		StringBuilder sb = null;
		String str = Objects.toString(obj, "");
		for (int i = 0; i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			String after = transfer(str, i, true, false, false);
			if (after != null) {
				if (sb == null) {
					sb = new StringBuilder(str.substring(0, i));
				}
				sb.append(after);
			} else {
				if (sb != null) {
					sb.appendCodePoint(str.codePointAt(i));
				}
			}
		}
		return Objects.toString(sb, null);
	}

	/**
	 * コントロールコード除去
	 * @param val 文字列
	 * @return 除去後文字列
	 */
	public static String noControl(final String val) {
		return Objects.toString(val, "").codePoints().
				filter(c -> (c > 31 && c != 127) || c == 9).
				collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).
				toString();
	}

}
