package project.common;

import java.lang.Character.UnicodeBlock;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
import java.util.function.IntPredicate;
import java.util.function.Predicate;
import java.util.regex.Pattern;

import org.apache.logging.log4j.LogManager;

import core.util.DateUtil;
import core.util.MojiUtil;
import core.util.NumberUtil;
import core.util.bean.Deal;

/**
 * チェックユーティリティ
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class CheckUtil {

	/** 数値判定パターン */
	private static final Pattern NUMERAL = Pattern.compile("^((-?[1-9][0-9]*)|0)$");

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

	/**
	 * nullまたは空文字判定
	 *
	 * @param str 文字列
	 * @return 文字列がnullまたは空文字の場合 true を返す。
	 */
	public static boolean isEmpty(final String str) {
		return Objects.toString(str, "").isEmpty();
	}

	/**
	 * null、空文字または空白のみ判定
	 *
	 * @param str 文字列
	 * @return 文字列がnull、空文字または空白のみの場合 true を返す。
	 */
	public static boolean isTrimEmpty(final String str) {
		return Objects.toString(str, "").trim().isEmpty();
	}

	/**
	 * null、空文字または空白のみ（含む全角）判定
	 *
	 * @param str 文字列
	 * @return 文字列がnull、空文字または空白のみ（含む全角）の場合 true を返す。
	 */
	public static boolean isZenTrimEmpty(final String str) {
		return Objects.toString(str, "").replaceAll("[　 ]", "").trim().isEmpty();
	}

	/**
	 * 指定したバイト数判定
	 *
	 * @param str 文字列
	 * @param len バイト数
	 * @param charset エンコード
	 * @return	文字列が指定したバイト数である場合 true を返す。
	 */
	public static boolean isJustByte(final String str, final int len, final Charset charset) {
		final boolean ret;
		if (isEmpty(str)) {
			ret = 0 == len;
		} else {
			if (charset != null) {
				ret = str.getBytes(charset).length == len;
			} else {
				ret = str.getBytes(StandardCharsets.ISO_8859_1).length == len;
			}
		}
		return ret;
	}

	/**
	 * 指定したバイト数以内判定
	 *
	 * @param str 文字列
	 * @param len バイト数
	 * @param charset エンコード
	 * @return	指定バイト数内の場合 true を返す。
	 */
	public static boolean isLessByte(final String str, final int len, final Charset charset) {
		final boolean ret;
		if (isEmpty(str)) {
			ret = 0 < len;
		} else {
			// バイト数チェック
			if (charset != null) {
				ret = str.getBytes(charset).length < len;
			} else {
				ret = str.getBytes(StandardCharsets.ISO_8859_1).length < len;
			}
		}
		return ret;
	}

	/**
	 * 行数・行バイト数判定 "\r\n"までを一行とみなす。
	 *
	 * @param str 対象文字列
	 * @param len 一行のバイト数
	 * @param charset エンコード
	 * @return 全行が指定数内の場合 true を返す。
	 */
	public static boolean isLessByteCrLf(
			final String str, final int len, final Charset charset) {
		if (!isEmpty(str)) {
			for (final var line : str.split(MojiUtil.RET_CODE_CRLF, -1)) {
				if (!isLessByte(line, len, charset)) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * 桁数(文字数)判定
	 *
	 * @param str 入力文字列
	 * @param len 桁数(文字数)
	 * @return	桁数が一致した場合 true を返す。
	 */
	public static boolean isJustLength(final String str, final int len) {
		final boolean ret;
		if (isEmpty(str)) {
			ret = 0 == len;
		} else {
			ret = str.codePointCount(0, str.length()) == len;
		}
		return ret;
	}

	/**
	 * 入力項目の桁数(文字数)判定
	 *
	 * @param str 入力文字列
	 * @param len 桁数(文字数)
	 * @return	桁数が指定桁数未満の場合 true を返す。
	 */
	public static boolean isLessLength(final String str, final int len) {
		final boolean ret;
		if (isEmpty(str)) {
			ret = 0 < len;
		} else {
			ret = str.codePointCount(0, str.length()) < len;
		}
		return ret;
	}

	/**
	 * 入力項目の桁数(文字数)判定
	 *
	 * @param str 入力文字列
	 * @param len 桁数(文字数)
	 * @return	桁数が指定桁数未満の場合 true を返す。
	 */
	public static boolean isLessEqualLength(final String str, final int len) {
		final boolean ret;
		if (isEmpty(str)) {
			ret = 0 <= len;
		} else {
			ret = str.codePointCount(0, str.length()) <= len;
		}
		return ret;
	}

	/**
	 * 日付チェック
	 * @param val 年月日
	 * @return 正しい場合 true を返す。
	 */
	public static boolean isDate(final String val) {
		final Predicate<String> isDate = s -> {
			final var str = DateUtil.removeSeparator(s);
			return (str.length() == 8) && (DateUtil.parseDate(str, DateUtil.FORMAT_DATE) != null);
		};
		return isEmpty(val) || isDate.test(val);
	}

	/**
	 * タイムスタンプの妥当性を判定
	 *
	 * @param val 年月日時分秒ミリ秒
	 * @return タイムスタンプとして正しい場合 true を返す。
	 */
	public static boolean isDateTime(final String val) {
		final Predicate<String> isDateTime = s -> {
			final var str = DateUtil.removeSeparator(s);
			return Deal.of(4 <= str.length() && str.length() <= 23
					&& DateUtil.parseDate(str, DateUtil.FORMAT_DATE_TIME) != null
				).then(() -> {
					final var len = DateUtil.FORMAT_DATE_TIME.length();
					return str.length() <= len || isNumber(str.substring(len));
				}).result();
		};
		return isEmpty(val) || isDateTime.test(val);
	}

	/**
	 * 年月日順序判定
	 *
	 * @param start 開始年月日 'yyyyMMdd'
	 * @param end 終了年月日 'yyyyMMdd'
	 * @return 正常な順序ならば true を返す。
	 */
	public static boolean isDateOrder(final String start, final String end) {
		return (isEmpty(start) && isDateTime(end)) || (isEmpty(end) && isDateTime(start))
				|| (isDateTime(start) && isDateTime(end) && start.compareTo(end) <= 0);
	}

	/**
	 * 半角数字で少数点指定桁数以内判定
	 *
	 * @param str 入力文字列
	 * @param nlen 最大整数部桁数
	 * @param dlen 最大小数部桁数
	 * @return 半角数字で少数点指定桁数の場合 true を返す。
	*/
	public static boolean isDecimal(final String str, final int nlen, final int dlen) {
		if (isEmpty(str)) {
			return true;
		}

		final var target = str.replace(",", "");
		if (target.isEmpty()) {
			// 必須でエラーにならないため
			return false;
		}

		String re;
		if (0 < nlen) {
			re = String.format("^((-?[1-9][0-9]{0,%d})|-?0)", nlen - 1);
		} else {
			re = "^(-?0)";
		}
		if (0 < dlen) {
			re = re + String.format("(\\.[0-9]{1,%d})?$", dlen);
		} else {
			re = re + "$";
		}
		final var pattern = Pattern.compile(re);
		return pattern.matcher(target).matches();
	}

	/**
	 * 半角数字("0"から"9")判定
	 *
	 * @param num 入力文字列
	 * @return 半角数字の場合 true を返す。
	 */
	public static boolean isNumber(final String num) {
		return isEmpty(num) || Pattern.compile("^[0-9]+$").matcher(num).matches();
	}

	/**
	 * 電話番号の妥当性判定
	 *
	 * @param str 文字列
	 * @return	電話番号が妥当の場合 true を返す。
	 */
	public static boolean isTelephoneNumber(final String str) {
		return isEmpty(str)
			|| (5 <= str.length() && str.length() <= 14
				&& Pattern.compile("^[0-9]+-[0-9]+-[0-9]+$").matcher(str).matches());
	}

	/**
	 * 金額判定
	 *
	 * @param val 金額文字列
	 * @return 正常の場合 true を返す。
	 */
	public static boolean isMoneyNumber(final String val) {
		final Predicate<String> isMoney = s -> {
			final var pattern = Pattern.compile(
					"^(-?[1-9][0-9]*|0)(\\.[0-9]+)?|(-0\\.[0-9]*[1-9][0-9]*)$");
			return pattern.matcher(s).matches();
		};
		return isEmpty(val) || isMoney.test(val);
	}

	/**
	 * メールアドレスの書式判定
	 *
	 * @param str 文字列
	 * @return メールアドレス書式に一致した場合 true を返す。
	 */
	public static boolean isEmailAddress(final String str) {
		return isEmpty(str)
			|| Pattern.compile("^[\\w.\\-]+@(?:[\\w\\-]+\\.)+[\\w\\-]+$").matcher(str).matches();
	}

	/**
	 * 郵便番号チェック
	 *
	 * @param val 郵便番号文字列（ハイフン入り）
	 * @return 正常の場合 true を返す。
	 */
	public static boolean isPostcode(final String val) {
		return isEmpty(val) || Pattern.compile("^[0-9]{3}-[0-9]{4}$").matcher(val).matches();
	}

	/**
	 * URL判定
	 *
	 * @param str 文字列
	 * @return	正しいURLの場合 true を返す。
	 */
	public static boolean isUrl(final String str) {
		try {
			if (!isEmpty(str)) {
				final var url = new URL(str);
				LogManager.getLogger().debug(url);
			}
			return true;
		} catch (final MalformedURLException e) {
			LogManager.getLogger().info(e.getMessage());
			return false;
		}
	}

	/**
	 * 半角数字("0"から"9")と負値判定
	 *
	 * @param num 入力文字列
	 * @return 半角数字の場合 true を返す。
	 */
	public static boolean isInteger(final String num) {
		return isEmpty(num)
				|| (NUMERAL.matcher(num).matches() && NumberUtil.toInteger(num) != null);
	}

	/**
	 * 半角数字("0"から"9")と負値判定
	 *
	 * @param num 入力文字列
	 * @return 半角数字の場合 true を返す。
	 */
	public static boolean isLong(final String num) {
		return isEmpty(num) || (NUMERAL.matcher(num).matches() && NumberUtil.toLong(num) != null);
	}

	/**
	 * パターンマッチング判定
	 *
	 * @param data 対象文字列
	 * @param ptn パターン("[]"で囲んだ形式)
	 * @return マッチした場合 true を返す。
	 */
	public static boolean isMatched(final String data, final String ptn) {
		return isEmpty(data) || (!isEmpty(ptn) && Pattern.compile(ptn).matcher(data).matches());
	}

	/**
	 * 半角英数字判定
	 * 文字列が'A'から'Z', 'a'から'z', '0'から'9'であるか判定
	 *
	 * @param str 文字列
	 * @return 半角英数字の場合 true を返す。
	 */
	public static boolean isHanEisu(final String str) {
		return isEmpty(str) || Pattern.compile("^[0-9a-zA-Z]*$").matcher(str).matches();
	}

	/**
	 * 半角文字判定
	 *
	 * @param str 入力文字列
	 * @return 半角文字の場合 true を返す。
	 */
	public static boolean isHankaku(final String str) {
		final IntPredicate hankaku = cp -> UnicodeBlock.of(cp) == UnicodeBlock.BASIC_LATIN;
		return Optional.ofNullable(str).map(s -> s.chars().allMatch(hankaku)).orElse(true);
	}

	/**
	 * 半角カナ（濁点、半濁点）存在判定
	 *
	 * @param str 文字列
	 * @return	半角カナが存在している場合 true を返す。
	 */
	public static boolean hasHanKana(final String str) {
		for (var i = 0; str != null && i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			// Unicode半角カタカナのコード範囲か判別
			final var cp = str.codePointAt(i);
			if (0xff61 <= cp && cp <= 0xff9f) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 半角カナ（濁点、半濁点）判定
	 *
	 * @param str 文字列
	 * @return	半角カナの場合 true を返す。
	 */
	public static boolean isHanKana(final String str) {
		for (var i = 0; str != null && i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			// Unicode半角カタカナのコード範囲か判別
			final var cp = str.codePointAt(i);
			if (cp < 0xff61 || 0xff9f < cp) {
				return false;
			}
		}
		return true;
	}

	/**
	 * ひらがな判定 ("ー")("－")("・")はひらがなとみなす。
	 *
	 * @param str 対象文字列
	 * @return ひらがなの場合 true を返す。
	 */
	public static boolean isHirakana(final String str) {
		for (var i = 0; str != null && i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			final var c = str.codePointAt(i);
			if (UnicodeBlock.of(c) != UnicodeBlock.HIRAGANA) {
				if (c != 'ー' && c != '－' && c != '・') {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * 文字列に半角全角文字が混在しているか判定
	 *
	 * @param str 文字列
	 * @return	半角全角が混在している場合 true を返す。
	 */
	public static boolean hasMultiWidth(final String str) {
		var half = false;
		var full = false;
		for (int i = 0; str != null && i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			if (UnicodeBlock.of(str.codePointAt(i)) == UnicodeBlock.BASIC_LATIN) {
				half = true;
			} else {
				full = true;
			}
		}
		return half && full;
	}

	/**
	 * 全角カナ判定
	 *
	 * @param str 文字列
	 * @return	全角カナのみでの場合 true を返す。
	 */
	public static boolean isZenKatakana(final String str) {
		final IntPredicate zenkaku = cp -> UnicodeBlock.of(cp) == UnicodeBlock.KATAKANA;
		return Optional.ofNullable(str).map(s -> s.chars().allMatch(zenkaku)).orElse(true);
	}

	/**
	 * 全角文字判定
	 *
	 * @param str 文字列
	 * @return	全て全角文字の場合 true を返す。
	 */
	public static boolean isZenkaku(final String str) {
		for (var i = 0; str != null && i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			final var ch = str.codePointAt(i);
			if (UnicodeBlock.of(ch) == UnicodeBlock.BASIC_LATIN) {
				if (ch != '\r' && ch != '\n' && ch != '\t') {
					return false;
				}
			}
		}
		return true;
	}
}
