package thyyxxk.webserver.utils;

import lombok.extern.slf4j.Slf4j;
import thyyxxk.webserver.constants.Gender;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @description: IdCard functions
 * @author: DingJie
 * @create: 2021-04-01 11:42:13
 **/
@Slf4j
public class IdCardUtil {
    /**
     * 每位加权因子
     * */
    private static final int[] POWER = { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 };
    private static final int MAX_LENGTH = 18;
    private static final int MIN_LENGTH = 15;
    /** 台湾身份首字母对应数字 */
    public static Map<String, Integer> twFirstCode = new HashMap<>();
    /** 香港身份首字母对应数字 */
    public static Map<String, Integer> hkFirstCode = new HashMap<>();
    static {
        twFirstCode.put("A", 10);
        twFirstCode.put("B", 11);
        twFirstCode.put("C", 12);
        twFirstCode.put("D", 13);
        twFirstCode.put("E", 14);
        twFirstCode.put("F", 15);
        twFirstCode.put("G", 16);
        twFirstCode.put("H", 17);
        twFirstCode.put("J", 18);
        twFirstCode.put("K", 19);
        twFirstCode.put("L", 20);
        twFirstCode.put("M", 21);
        twFirstCode.put("N", 22);
        twFirstCode.put("P", 23);
        twFirstCode.put("Q", 24);
        twFirstCode.put("R", 25);
        twFirstCode.put("S", 26);
        twFirstCode.put("T", 27);
        twFirstCode.put("U", 28);
        twFirstCode.put("V", 29);
        twFirstCode.put("X", 30);
        twFirstCode.put("Y", 31);
        twFirstCode.put("W", 32);
        twFirstCode.put("Z", 33);
        twFirstCode.put("I", 34);
        twFirstCode.put("O", 35);
        hkFirstCode.put("A", 1);
        hkFirstCode.put("B", 2);
        hkFirstCode.put("C", 3);
        hkFirstCode.put("R", 18);
        hkFirstCode.put("U", 21);
        hkFirstCode.put("Z", 26);
        hkFirstCode.put("X", 24);
        hkFirstCode.put("W", 23);
        hkFirstCode.put("O", 15);
        hkFirstCode.put("N", 14);
    }

    /**
     * 验证所有的身份证的合法性
     */
    public static boolean isValidatedIdCard(String idCard) {
        if (StringUtil.isBlank(idCard)) {
            return false;
        }
        if (idCard.length() == MIN_LENGTH) {
            idCard = convertIdCarBy15bit(idCard);
        }
        if (StringUtil.isBlank(idCard)) {
            return false;
        }
        return validateIdCard18(idCard) || validateIdCard10(idCard);
    }

    private static boolean validateIdCard18(String idCard) {
        // 非18位为假
        if (idCard.length() != MAX_LENGTH) {
            return false;
        }
        // 获取前17位
        String idCard17 = idCard.substring(0, 17);
        // 获取第18位
        String idCard18Code = idCard.substring(17, 18);
        char[] c;
        String checkCode;
        // 是否都为数字
        if (isDigital(idCard17)) {
            c = idCard17.toCharArray();
        } else {
            return false;
        }
        int[] bit;
        bit = convertCharToInt(c);
        int sum17;
        sum17 = getPowerSum(bit);
        // 将和值与11取模得到余数进行校验码判断
        checkCode = getCheckCodeBySum(sum17);
        if (null == checkCode) {
            return false;
        }
        // 将身份证的第18位与算出来的校码进行匹配，不相等就为假
        return idCard18Code.equalsIgnoreCase(checkCode);
    }

    /**
     * 将15位的身份证转成18位身份证
     */
    private static String convertIdCarBy15bit(String idCard) {
        String idCard17;
        // 非15位身份证
        if (idCard.length() != MIN_LENGTH) {
            return null;
        }
        if (isDigital(idCard)) {
            // 获取出生年月日
            String birthday = idCard.substring(6, 12);
            Date birthdate = null;
            try {
                birthdate = new SimpleDateFormat("yyMMdd").parse(birthday);
            } catch (ParseException e) {
                log.error("15位身份证转18位解析出生日期出错", e);
            }
            if (null == birthdate) {
                return null;
            }
            Calendar cday = Calendar.getInstance();
            cday.setTime(birthdate);
            String year = String.valueOf(cday.get(Calendar.YEAR));
            idCard17 = idCard.substring(0, 6) + year + idCard.substring(8);
            char[] c = idCard17.toCharArray();
            String checkCode;
            int[] bit;
            // 将字符数组转为整型数组
            bit = convertCharToInt(c);
            int sum17;
            sum17 = getPowerSum(bit);
            // 获取和值与11取模得到余数进行校验码
            checkCode = getCheckCodeBySum(sum17);
            // 获取不到校验位
            if (null == checkCode) {
                return null;
            }
            // 将前17位与第18位校验码拼接
            idCard17 += checkCode;
        } else { // 身份证包含数字
            return null;
        }
        return idCard17;
    }

    /**
     * 数字验证
     */
    private static boolean isDigital(String str) {
        return str != null && !"".equals(str) && str.matches("^[0-9]*$");
    }

    /**
     * 将身份证的每位和对应位的加权因子相乘之后，再得到和值
     */
    private static int getPowerSum(int[] bit) {
        int sum = 0;
        if (POWER.length != bit.length) {
            return sum;
        }
        for (int i = 0; i < bit.length; i++) {
            for (int j = 0; j < POWER.length; j++) {
                if (i == j) {
                    sum = sum + bit[i] * POWER[j];
                }
            }
        }
        return sum;
    }

    /**
     * 将和值与11取模得到余数进行校验码判断
     */
    private static String getCheckCodeBySum(int sum17) {
        String checkCode;
        switch (sum17 % 11) {
            case 10:
                checkCode = "2";
                break;
            case 9:
                checkCode = "3";
                break;
            case 8:
                checkCode = "4";
                break;
            case 7:
                checkCode = "5";
                break;
            case 6:
                checkCode = "6";
                break;
            case 5:
                checkCode = "7";
                break;
            case 4:
                checkCode = "8";
                break;
            case 3:
                checkCode = "9";
                break;
            case 2:
                checkCode = "x";
                break;
            case 1:
                checkCode = "0";
                break;
            case 0:
            default:
                checkCode = "1";
                break;
        }
        return checkCode;
    }

    /**
     * 将字符数组转为整型数组
     */
    private static int[] convertCharToInt(char[] c) throws NumberFormatException {
        int[] a = new int[c.length];
        int k = 0;
        for (char temp : c) {
            a[k++] = Integer.parseInt(String.valueOf(temp));
        }
        return a;
    }

    public static Gender getGenderByIdCard(String idCard) {
        if (idCard.length() < 18) {
            return Gender.UNKNOWN;
        }
        int identifyCode = idCard.charAt(16);
        return identifyCode % 2 == 0 ? Gender.FEMALE : Gender.MALE;
    }

    public static Integer getAgeByIdCard(String idCard) {
        if (idCard.length() < 18) {
            return null;
        }
        String birthDay = idCard.substring(6, 14);
        String time = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        String yearStr = time.split("-")[0];
        String monthStr = time.split("-")[1];
        String dayStr = time.split("-")[2];
        String yearBirthStr = birthDay.substring(0, 4);
        String monthBirthStr = birthDay.substring(4, 6);
        String dayBirthStr = birthDay.substring(6);
        int year = Integer.parseInt(yearStr);
        int yearBirth = Integer.parseInt(yearBirthStr);
        if (year - yearBirth <= 0) {
            return 0;
        }
        int age = year - yearBirth;
        int month = Integer.parseInt(monthStr);
        int monthBirth = Integer.parseInt(monthBirthStr);
        if (month - monthBirth > 0) {
            return age;
        }
        if (month - monthBirth < 0) {
            return --age;
        }
        int day = Integer.parseInt(dayStr);
        int dayBirth = Integer.parseInt(dayBirthStr);
        if (day - dayBirth >= 0) {
            return age;
        }
        return --age;
    }

    /**
     * 验证10位身份证编码是否合法
     */
    private static boolean validateIdCard10(String idCard) {
        String card = idCard.replaceAll("\\(", "").replaceAll("\\)", "");
        if (card.length() != 8 && card.length() != 9 && idCard.length() != 10) {
            return false;
        }
        if (idCard.matches("^[A-Z]{1,2}[0-9]{6}\\(?[0-9A]\\)?$")) { // 香港
            return validateHKCard(idCard);
        }
        if (idCard.matches("^[1|5|7][0-9]{6}\\(?[0-9A-Z]\\)?$")) { // 澳门，目前没做校验，澳门的直接返回true
            return true;
        }
        if (idCard.matches("^[a-zA-Z][0-9]{9}")) { // 台湾
            String char2 = idCard.substring(1, 2);
            if (!char2.equals("1") && !char2.equals("2")) {
                return false;
            }
            return validateTWCard(idCard);
        }
        return false;
    }

    /**
     * 验证台湾身份证号码
     * 共10位
     * 计算校验码的步骤：1、英文转成数字，其个位数字*9+十位数
     *         2、中间八位数字，各数字从左到右依次乘以8、7、6、5....2、1
     *         3、计算步骤1和2的和sum
     *         4、计算10-sum%10 ,其结果就是检查码，若余数为0，则检查码就是0
     * @param idCard
     *            身份证号码
     * @return 验证码是否符合
     */
    private static boolean validateTWCard(String idCard) {
        //第一位是字母（表示地区代码），通过map找到对应的数字代码
        String start = idCard.substring(0, 1);
        //第二位数字表示性别，男生为1，女生为2
        String mid = idCard.substring(1, 9);
        //最后一位是检查码,由第一位和8个数字的组合计算得到校验和
        String end = idCard.substring(9, 10);
        Integer iStart = twFirstCode.get(start);

        int sum = iStart / 10 + (iStart % 10) * 9;
        char[] chars = mid.toCharArray();
        int iflag = 8;
        for (char c : chars) {
            sum = sum + Integer.parseInt(c + "") * iflag;
            iflag--;
        }
        return (sum % 10 == 0 ? 0 : (10 - sum % 10)) == Integer.parseInt(end);
    }

    /**
     * 验证香港身份证号码(存在Bug，部份特殊身份证无法检查)
     *
     * 如：身份证号码是：C668668（9）
     * 香港身份证号码由三部分组成：一个英文字母；6个数字；括号及0-9中的任一个数字，或者字母A。
     * 括号中的数字或字母A，是校验码，用于检验括号前面的号码的逻辑正确性。
     *
     * 身份证前2位为英文字符，如果只出现一个英文字符则表示第一位是空格，对应数字58, 前2位英文字符A-Z分别对应数字10-35
     * 最后一位校验码为0-9的数字加上字符"A"，"A"代表10
     * 将身份证号码全部转换为数字，分别对应乘9、8...1相加的总和，整除11则证件号码有效
     *
     * @param idCard
     *            身份证号码
     * @return 验证码是否符合
     */
    private static boolean validateHKCard(String idCard) {
        String card = idCard.replaceAll("\\(", "").replaceAll("\\)", "");
        int sum = 0;
        if (card.length() == 9) {
            sum = ((int) card.charAt(0) - 55) * 9 + ((int) card.charAt(1) - 55) * 8;
            card = card.substring(1, 9);
        } else {
            sum = 522 + ((int) card.charAt(0) - 55) * 8;
        }
        String mid = card.substring(1, 7);
        String end = card.substring(7, 8);
        char[] chars = mid.toCharArray();
        int iflag = 7;
        for (char c : chars) {
            sum = sum + Integer.parseInt(c + "") * iflag;
            iflag--;
        }
        if (end.equalsIgnoreCase("A")) {
            sum = sum + 10;
        } else {
            sum = sum + Integer.parseInt(end);
        }
        return sum % 11 == 0;
    }
}
