package online.view;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import javax.servlet.ServletResponse;

import core.config.Factory;
import core.util.MojiUtil;
import core.util.NumberUtil;
import online.view.model.ViewMap;

/**
 * 表示用ユーティリティ
 *
 * @author Tadashi Nakayama
 */
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) {
		final var keyword = "charset=";

		final var str = response.getContentType();
		if (str != null) {
			final var 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) {
		final var mpg = getMapping(response);
		return Optional.ofNullable(mpg).map(Charset::forName).orElse(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 (var i = 0; str != null && i < str.length(); i = str.offsetByCodePoints(i, 1)) {
				var after = transfer(str, i, xml, sanitize);
				if (after == null && mapping != null) {
					final var a = str.codePointAt(i);
					final var b = MojiUtil.correctGarbled(a, mapping);
					if (a != b) {
						after = String.valueOf(Character.toChars(b));
					}
				}

				sb = MojiUtil.addBuilder(sb, str, i, after, str.codePointAt(i));
			}
		}
		return Objects.toString(sb, str);
	}

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

		final var 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) {
				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) {
		final var 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;
		var 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) {
				final var a = str.codePointAt(i);
				final var b = MojiUtil.correctGarbled(a, mapping);
				if (a != b) {
					after = String.valueOf(Character.toChars(b));
				}
			}

			sb = MojiUtil.addBuilder(sb, str, i, after, 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 Collection.class.cast(col).contains(val);
			} else if (col.getClass().isArray()) {
				return Stream.of(Object[].class.cast(col)).anyMatch(Predicate.isEqual(val));
			} else if (String.class.isInstance(col)) {
				return String.class.cast(col).contains(val.toString());
			}
		}
		return Boolean.FALSE;
	}

	/**
	 * サニタイズ処理
	 *
	 * @param obj 対象オブジェクト
	 * @return サニタイズ文字列
	 */
	public static String escape(final Object obj) {
		StringBuilder sb = null;
		final var str = Objects.toString(obj, "");
		for (var i = 0; i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			final var after = transfer(str, i, true, false);
			sb = MojiUtil.addBuilder(sb, str, i, after, 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();
	}

	/**
	 * 値分割処理
	 *
	 * @param value 入力値
	 * @return 分割後配列
	 */
	public static String[] splitValue(final String value) {
		return Optional.ofNullable(value).map(v -> v.split(",", -1)).orElse(new String[0]);
	}


	/**
	 * オブジェクト取得
	 *
	 * @param vm ViewMap
	 * @param key キー
	 * @return オブジェクト
	 */
	public static Object getObject(final Map<String, Serializable> vm, final String key) {
		var map = vm;
		final var keys = key.split("\\.");
		for (var i = 0; i < keys.length - 1; i++) {
			final var obj = map.get(keys[i]);
			if (!Map.class.isInstance(obj)) {
				return null;
			}
			map = Factory.cast(obj);
		}

		return map.get(keys[keys.length - 1]);
	}

	/**
	 * 配列取得名化
	 *
	 * @param name 項目名
	 * @return 配列取得名
	 */
	public static String toArrayName(final String name) {
		final var ret = new StringBuilder();
		final var loc = name.lastIndexOf('.');
		if (0 <= loc) {
			ret.append(name, 0, loc + ".".length());
		}
		ret.append(ViewMap.ATTR_ARRAY).append(".");
		ret.append(name.substring(loc + ".".length()));
		return ret.toString();
	}
}
