package project.common;

import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.IntStream;

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

/**
 * 文字列操作ユーティリティ
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class StringUtil {

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

	/**
	 * 文字列内に指定文字列を挿入する。（文字列の前後には挿入しない。）
	 * @param org 元文字列
	 * @param val 挿入文字列
	 * @param loc 挿入位置
	 * @return 挿入後文字列
	 */
	public static String infix(final String org, final String val, final int... loc) {
		if (org != null && val != null && !val.isEmpty() && 0 < loc.length) {
			final var count = org.codePointCount(0, org.length());
			if (1 < count) {
				final var sb = new StringBuilder();
				sb.appendCodePoint(org.codePointAt(0));

				for (int i = org.offsetByCodePoints(0, 1), k = 1; i < org.length();
						i = org.offsetByCodePoints(i, 1), k++) {
					for (final var l : loc) {
						if (k == l || k - count == l) {
							sb.append(val);
							break;
						}
					}
					sb.appendCodePoint(org.codePointAt(i));
				}

				return sb.toString();
			}
		}
		return org;
	}

	/**
	 * 部分文字列取得
	 * @param str 文字列
	 * @param begin 開始位置
	 * @return 部分文字列
	 */
	public static String substring(final String str, final int begin) {
		if (str != null && 0 < begin && begin <= str.length()) {
			return str.substring(str.offsetByCodePoints(0, begin));
		}
		return str;
	}

	/**
	 * 部分文字列取得
	 * @param str 文字列
	 * @param begin 開始位置
	 * @param end 終了位置
	 * @return 部分文字列
	 */
	public static String substring(final String str, final int begin, final int end) {
		if (str != null && 0 < begin && begin <= str.length()
				&& begin <= end && end <= str.length()) {
			final var loc = str.offsetByCodePoints(0, begin);
			return str.substring(loc, str.offsetByCodePoints(loc, end - begin));
		}
		return str;
	}

	/**
	 * ベース名取得
	 *
	 * @param str 取得対象文字列
	 * @param delimiter ベース区切り文字
	 * @return ベース名
	 */
	public static String basename(final String str, final String delimiter) {
		var loc = str.lastIndexOf(delimiter);
		if (0 <= loc) {
			loc += delimiter.length();
		} else {
			loc = 0;
		}
		return str.substring(loc);
	}

	/**
	 * ランダム文字列生成
	 *
	 * @param len 必要文字列長
	 * @return ランダム文字列
	 */
	public static String randomization(final int len) {
		final var sb = new StringBuilder();
		while (len <= sb.length()) {
			final var num = BigDecimal.valueOf(
					ThreadLocalRandom.current().nextDouble()).longValue();
			sb.append(Long.toString(num, 36));
		}
		return sb.toString().substring(0, len);
	}

	/**
	 * 分割
	 * @param str 文字列
	 * @param regex 分割文字列
	 * @return 分割文字列配列
	 */
	public static String[] split(final String str, final String regex) {
		return split(str, regex, 0);
	}

	/**
	 * 分割
	 * @param str 文字列
	 * @param regex 分割文字列
	 * @param limit リミット
	 * @return 分割文字列配列
	 */
	public static String[] split(final String str, final String regex, final int limit) {
		return (str != null) ? str.split(regex, limit) : new String[0];
	}

	/**
	 * 連番配列取得
	 *
	 * @param len 長さ
	 * @return 連番配列
	 */
	public static String[] sequence(final int len) {
		return sequence(len, 0);
	}

	/**
	 * 連番配列取得
	 *
	 * @param len 長さ
	 * @param org 開始番号
	 * @return 連番配列
	 */
	public static String[] sequence(final int len, final int org) {
		return IntStream.range(0, Math.max(len, 0)).
			mapToObj(i -> String.valueOf(i + org)).toArray(String[]::new);
	}

	/**
	 * トリム処理
	 * @param vals 値配列
	 * @return トリム後値配列
	 */
	public static String[] trim(final String... vals) {
		for (var i = 0; vals != null && i < vals.length; i++) {
			if (vals[i] != null) {
				vals[i] = vals[i].trim();
			}
		}
		return vals;
	}

	/**
	 * 日時フォーマット
	 *
	 * @param val オブジェクト
	 * @param ptn パターン
	 * @return フォーマット済日時文字列
	 */
	public static String formatDate(final Object val, final String ptn) {
		if (Date.class.isInstance(val)) {
			return DateUtil.format(Date.class.cast(val), ptn);
		} else if (String.class.isInstance(val)) {
			final var date = DateUtil.toDate(String.class.cast(val));
			return (date != null) ? DateUtil.format(date, ptn) : String.class.cast(val);
		}
		return null;
	}

	/**
	 * 時刻フォーマット
	 *
	 * @param val オブジェクト
	 * @param ptn パターン
	 * @return フォーマット済日時文字列
	 */
	public static String formatTime(final Object val, final String ptn) {
		if (Date.class.isInstance(val)) {
			return DateUtil.format(Date.class.cast(val), ptn);
		} else if (String.class.isInstance(val)) {
			final var date = DateUtil.toTime(String.class.cast(val));
			return (date != null) ? DateUtil.format(date, ptn) : String.class.cast(val);
		}
		return null;
	}

	/**
	 * 数字フォーマット
	 * @param val オブジェクト
	 * @param ptn パターン
	 * @return フォーマット済文字列
	 */
	public static String formatNumber(final Object val, final String ptn) {
		if (!Objects.toString(ptn, "").isEmpty()) {
			if (Number.class.isInstance(val)) {
				return new DecimalFormat(ptn).format(val);
			} else if (String.class.isInstance(val)) {
				final var bd = NumberUtil.toBigDecimal(String.class.cast(val));
				return (bd != null) ? new DecimalFormat(ptn).format(bd) : String.class.cast(val);
			}
		}
		return null;
	}

	/**
	 * 数値文字列をゼロサプレスする。
	 *
	 * @param str 対象文字列
	 * @return ゼロサプレス後文字列
	 */
	public static String suppressZero(final String str) {
		if (Objects.toString(str, "").trim().isEmpty()) {
			return str;
		}

		final var target = str.trim().replace(",", "").split("\\.");
		for (var i = 0; i < target.length; i++) {
			if (1 < i || !CheckUtil.isNumber(target[i])) {
				return str;
			}
		}

		var len = 0;
		if (!target[0].isEmpty()) {
			len = target[0].offsetByCodePoints(target[0].length(), -1);
		}

		var i = 0;
		while (i < len && target[0].codePointAt(i) == '0') {
			i = target[0].offsetByCodePoints(i, 1);
		}
		target[0] = target[0].substring(i);

		return String.join(".", target);
	}

	/**
	 * 数値文字列にカンマを挿入する。
	 *
	 * @param str 文字列
	 * @return カンマ編集文字列
	 */
	public static String insertComma(final String str) {
		if (Objects.toString(str, "").trim().isEmpty()) {
			return "";
		}

		var target = str.trim().replace(",", "");
		var dec = "";
		final var loc = target.indexOf('.');
		if (0 <= loc) {
			dec = target.substring(loc + ".".length());
			target = target.substring(0, loc);
		}

		final var sb = new StringBuilder();
		if (target.startsWith("-")) {
			sb.append("-");
			target = target.substring("-".length());
		}
		if (!CheckUtil.isNumber(target) || !CheckUtil.isNumber(dec)) {
			return str;
		}

		//カンマ追加
		if (3 < target.length()) {
			var offset = target.length() % 3;
			if (offset == 0) {
				offset = 3;
			}

			sb.append(target, 0, offset);
			for (var i = offset; i < target.length(); i += 3) {
				sb.append(",");
				sb.append(target, i, i + 3);
			}
		} else {
			sb.append(target);
		}

		if (!dec.isEmpty()) {
			sb.append(".").append(dec);
		}

		return sb.toString();
	}

	/**
	 * 文字列の左から指定文字を削除する。
	 * 文字列が"null"か空文字("")の場合、空文字("")を返す。
	 *
	 * @param str 入力文字列
	 * @param data 指定文字
	 * @return 返却文字列
	 */
	public static String trimLeft(final String str, final int... data) {
		if (Objects.toString(str, "").isEmpty()) {
			return "";
		}

		var i = 0;
	outer:
		while (i < str.length()) {
			for (final var d : data) {
				if (str.codePointAt(i) == d) {
					i = str.offsetByCodePoints(i, 1);
					continue outer;
				}
			}
			break;
		}

		return str.substring(i);
	}

	/**
	 * 文字列の右から指定文字を削除する。
	 * 文字列が"null"か空文字("")の場合、空文字("")を返す。
	 *
	 * @param str 入力文字列
	 * @param data 指定文字
	 * @return 返却文字列
	 */
	public static String trimRight(final String str, final int... data) {
		if (Objects.toString(str, "").isEmpty()) {
			return "";
		}

		var i = str.length();
	outer:
		while (0 < i) {
			for (final var d : data) {
				if (str.codePointBefore(i) == d) {
					i = str.offsetByCodePoints(i, -1);
					continue outer;
				}
			}
			break;
		}
		return str.substring(0, i);
	}

	/**
	 * 文字列の右に指定桁数になるように指定文字を追加します。
	 *
	 * @param str 対象文字列
	 * @param val 指定文字(1文字)
	 * @param length 全体文字数
	 * @return 追加後文字列
	 */
	public static String padRight(final String str, final int val, final int length) {
		var len = 0;
		if (str != null) {
			len = str.codePointCount(0, str.length());
		}
		if (length <= len) {
			return str;
		}

		final var sb = new StringBuilder();
		if (str != null) {
			sb.append(str);
		}

		final var chs = Character.toChars(val);
		for (int i = len; i < length; i += chs.length) {
			sb.append(chs);
		}

		return sb.toString();
	}

	/**
	 * 文字列の左に指定桁数になるように指定文字を追加する。
	 *
	 * @param str 対象文字列
	 * @param val 指定文字(1文字)
	 * @param length 全体文字数
	 * @return 追加後文字列
	 */
	public static String padLeft(final String str, final int val, final int length) {
		var len = 0;
		if (str != null) {
			len = str.codePointCount(0, str.length());
		}
		if (length <= len) {
			return str;
		}

		final var chs = Character.toChars(val);
		final var sb = new StringBuilder();
		for (int i = len; i < length; i += chs.length) {
			sb.append(chs);
		}

		if (str != null) {
			sb.append(str);
		}

		return sb.toString();
	}

	/**
	 * 文字列の右に指定バイト数になるように指定文字を追加する。
	 *
	 * @param str 対象文字列
	 * @param val 指定文字(1文字)
	 * @param length 全体バイト数
	 * @param charset エンコード
	 * @return 追加後文字列
	 */
	public static String padRightBytes(final String str, final int val,
			final int length, final Charset charset) {
		var len = 0;
		if (!Objects.toString(str, "").isEmpty()) {
			len = str.getBytes(charset).length;
		}

		final var size = String.valueOf(Character.toChars(val)).getBytes(charset).length;
		if (length <= len + size) {
			return str;
		}

		final var sb = new StringBuilder();
		sb.append(Objects.toString(str, ""));

		for (var i = len; i + size <= length; i += size) {
			sb.append(val);
		}

		return sb.toString();
	}

	/**
	 * 指定バイトになるよう文字列を切出す。
	 *
	 * @param str 対象文字列
	 * @param num 全体バイト数
	 * @param charset エンコード
	 * @return 切出し後文字列
	 */
	public static String limitBytes(final String str, final int num, final Charset charset) {
		var length = 0;
		var i = 0;
		while (str != null && i < str.length()) {
			final var e = str.offsetByCodePoints(i, 1);
			final var len = str.substring(i, e).getBytes(charset).length;
			if (num < length + len) {
				return str.substring(0, i);
			}
			length += len;
			i = e;
		}
		return str;
	}

	/**
	 * 指定エンコードで文字列の一致確認
	 *
	 * @param comp1 比較文字列１
	 * @param comp2 比較文字列２
	 * @param charset エンコード
	 * @return 一致した場合 true を返す。
	 */
	public static boolean equalsAsEncode(final String comp1,
			final String comp2, final Charset charset) {
		if (comp1 == null && comp2 == null) {
			return true;
		}

		if (comp1 != null && comp2 != null) {
			var i = 0;
			var j = 0;
			while (i < comp1.length() && j < comp2.length()) {
				if (MojiUtil.correctGarbled(comp1.codePointAt(i), charset)
						!= MojiUtil.correctGarbled(comp2.codePointAt(j), charset)) {
					return false;
				}
				i = comp1.offsetByCodePoints(i, 1);
				j = comp2.offsetByCodePoints(j, 1);
			}

			return i == comp1.length() && j == comp2.length();
		}

		return false;
	}

	/**
	 * 平仮名をカタカナに変換する。
	 *
	 * @param val 入力文字列
	 * @return 変換後文字列
	 */
	public static String hirakanaToKatakana(final String val) {
		StringBuilder sb = null;
		for (var i = 0; val != null && i < val.length(); i = val.offsetByCodePoints(i, 1)) {
			final var c = val.codePointAt(i);
			if (c >= 'ぁ' && c <= 'ん') {
				if (sb == null) {
					sb = new StringBuilder(val.substring(0, i));
				}
				sb.appendCodePoint(c - 'ぁ' + 'ァ');
			} else {
				if (sb != null) {
					sb.appendCodePoint(c);
				}
			}
		}
		return Objects.toString(sb, val);
	}

	/**
	 * 小文字を大文字に変換する。
	 * 小さい文字（ｯ､ｧ､ｨ､ｩ､ｪ､ｫ､ｬ､ｭ､ｮ）を大きい文字に変換する。
	 *
	 * @param item 対象文字列
	 * @return 変換した文字列
	 */
	public static String toBiggerHanKatakana(final String item) {
		StringBuilder sb = null;
		for (var i = 0; item != null && i < item.length(); i = item.offsetByCodePoints(i, 1)) {
			final var cp = item.codePointAt(i);
			String str = null;
			if (cp == 'ｯ') {
				str = "ﾂ";
			} else if (cp == 'ｧ') {
				str = "ｱ";
			} else if (cp == 'ｨ') {
				str = "ｲ";
			} else if (cp == 'ｩ') {
				str = "ｳ";
			} else if (cp == 'ｪ') {
				str = "ｴ";
			} else if (cp == 'ｫ') {
				str = "ｵ";
			} else if (cp == 'ｬ') {
				str = "ﾔ";
			} else if (cp == 'ｭ') {
				str = "ﾕ";
			} else if (cp == 'ｮ') {
				str = "ﾖ";
			}

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

	/**
	 * 数値としてソート
	 *
	 * @param arg 配列
	 * @return ソート後配列
	 */
	public static String[] sortAsNumeric(final String... arg) {
		final Comparator<String> comparator = Comparator.comparing(
			NumberUtil::toBigDecimal, Comparator.nullsLast(Comparator.naturalOrder()));

		final var opt = Optional.ofNullable(arg).map(String[]::clone);
		opt.ifPresent(a -> Arrays.sort(a, comparator));
		return opt.orElse(null);
	}
}
