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.regex.Pattern;

import org.apache.logging.log4j.LogManager;

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

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

	/** 数値判定パターン */
	private static final Pattern 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) {
		if (!isEmpty(str)) {
			// バイト数チェック
			if (charset != null) {
				return str.getBytes(charset).length == len;
			}
			return str.getBytes(StandardCharsets.ISO_8859_1).length == len;
		}
		return true;
	}

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

	/**
	 * 行数・行バイト数判定 "\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 String 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) {
		return isEmpty(str) || str.codePointCount(0, str.length()) == len;
	}

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

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

	/**
	 * 日付チェック
	 * @param val 年月日
	 * @return 正しい場合 true を返す。
	 */
	public static boolean isDate(final String val) {
		if (isEmpty(val)) {
			return true;
		}

		final String str = DateUtil.removeSeparator(val);
		return str.length() == 8 && DateUtil.parseDate(str, DateUtil.FORMAT_DATE) != null;
	}

	/**
	 * タイムスタンプの妥当性を判定
	 *
	 * @param val 年月日時分秒ミリ秒
	 * @return boolean タイムスタンプとして正しい場合 true を返す。
	 */
	public static boolean isDateTime(final String val) {
		if (isEmpty(val)) {
			return true;
		}

		final String str = DateUtil.removeSeparator(val);
		if (4 <= str.length() && str.length() <= 23) {
			if (DateUtil.parseDate(str, DateUtil.FORMAT_DATE_TIME) != null) {
				final int len = DateUtil.FORMAT_DATE_TIME.length();
				return str.length() <= len || isNumber(str.substring(len));
			}
		}
		return false;
	}

	/**
	 * 年月日順序判定
	 *
	 * @param start 開始年月日 'yyyyMMdd'
	 * @param end 終了年月日 'yyyyMMdd'
	 * @return 正常な順序ならば true を返す。
	 */
	public static boolean isDateOrder(final String start, final String end) {
		if (isEmpty(start)) {
			return isDateTime(end);
		} else if (isEmpty(end)) {
			return isDateTime(start);
		}
		return 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 String target = str.replace(",", "");
		if (target.isEmpty()) {
			// 必須でエラーにならないため
			return false;
		}

		String re = "";
		if (0 < nlen) {
			re = "^((-?[1-9][0-9]{0," + (nlen - 1) + "})|-?0)";
		} else {
			re = "^(-?0)";
		}
		if (0 < dlen) {
			re = re + "(\\.[0-9]{1," + dlen + "})?$";
		} else {
			re = re + "$";
		}
		final Pattern 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) {
		if (isEmpty(str)) {
			return true;
		}

		if (str.length() < 5 || 14 < str.length()) {
			return false;
		}

		final Pattern pattern = Pattern.compile("^[0-9]+-[0-9]+-[0-9]+$");
		return pattern.matcher(str).matches();
	}

	/**
	 * 金額判定
	 *
	 * @param val 金額文字列
	 * @return 正常の場合 true を返す。
	 */
	public static boolean isMoneyNumber(final String val) {
		if (isEmpty(val)) {
			return true;
		}

		final Pattern pattern = Pattern.compile(
				"^(-?[1-9][0-9]*|0)(\\.[0-9]+)?|(-0\\.[0-9]*[1-9][0-9]*)$");
		return pattern.matcher(val).matches();
	}

	/**
	 * メールアドレスの書式判定
	 *
	 * @param str 文字列
	 * @return メールアドレス書式に一致した場合 true を返す。
	 */
	public static boolean isEmailAddress(final String str) {
		if (isEmpty(str)) {
			return true;
		}

		final Pattern pattern = Pattern.compile(
				"^[\\w\\.\\-]+@(?:[\\w\\-]+\\.)+[\\w\\-]+$");
		return pattern.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 URL 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)
				|| (PATTERN_NUMERAL.matcher(num).matches() && NumberUtil.toInteger(num) != null);
	}

	/**
	 * 半角数字("0"から"9")と負値判定
	 *
	 * @param num 入力文字列
	 * @return 半角数字の場合 true を返す。
	 */
	public static boolean isLong(final String num) {
		return isEmpty(num)
				|| (PATTERN_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) {
		for (int i = 0; str != null && i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			if (UnicodeBlock.of(str.codePointAt(i)) != UnicodeBlock.BASIC_LATIN) {
				return false;
			}
		}
		return true;
	}

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

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

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