package cz.moneta.test.dsl.util.data.generators;

import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.exception.HarnessException;
import cz.moneta.test.harness.support.data.Generator;
import lombok.Getter;

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Class for calculate czech personal ID check digit.
 * Class imported from APM and modified for Harness usage
 *
 * Vypocita posledni kontrolni cislici na druhem radku strojove citelnych udaju na OP.
 * <p>
 * Vstupy:
 * DOC_NUMBER - cislo OP
 * PERSONAL_NUMBER - rodne cislo ve formatu RRMMDD1234
 * DATE_OF_EXPIRY - datum expirace OP ve formatu dd.MM.yyyy
 * <p>
 * Vystupy:
 * CHECK_SUM - kontrolni cislice
 *
 * @author Josef Mezera (123023137)
 * @since 14.10.2011
 *
 */
public class IdCardCheckDigitGenerator implements Generator<String> {

    private final int[] SCALES = {7, 3, 1};
    private final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyMMdd");

    @Override
    public String generate(StoreAccessor harness, Object... params) {
        LocalDate dateOfExpiry = Optional.ofNullable(params)
                .filter(p -> p.length == 3)
                .map(p -> p[2])
                .filter(LocalDate.class::isInstance)
                .map(LocalDate.class::cast)
                .orElseThrow(() -> new HarnessException("IdCardCheckDigitGenerator expect exactly 3 parameters"));

        return String.valueOf(calculateCheckDigit(String.valueOf(params[0]), String.valueOf(params[1]), dateOfExpiry));
    }

    public int calculateCheckDigit(String idNumber, String rc, LocalDate dateOfExpiry) {
        // validate inputs
        if (idNumber == null || idNumber.isEmpty() || rc == null || rc.isEmpty() || dateOfExpiry == null) {
            throw new HarnessException("Error during calculate IdCard check digit");
        }

        // parse data from inputs
        PersonalID pid = new PersonalID().parse(rc);
        String personalDate = DATE_FORMAT.format(pid.getBirthDate());
        String personalID = pid.getSecondPart();
        String expiryDate = dateOfExpiry.format(DateTimeFormatter.ofPattern("yyMMdd"));

        // RQ249072: uprava vypoctu kontrolni cislice u OP vydavanych od 1.1.2012
        if (idNumber.startsWith("2")) {
            personalID = "";
        }

        // calculate check numbers
        int idCheckNumber = calcCheckNumber(idNumber);
        int personalDateCheckNumber = calcCheckNumber(personalDate);
        int expiryCheckNumber = calcCheckNumber(expiryDate);

        // result
        return calcCheckNumber(
                createCompleteNumberLine(idNumber, personalDate, personalID, expiryDate, idCheckNumber, personalDateCheckNumber, expiryCheckNumber));
    }

    private int calcCheckNumber(String number) {
        int counter = 0;
        int result = 0;
        for (int i = 0; i < number.length(); i++) {
            int num = Integer.parseInt(Character.toString(number.charAt(i)));
            result = result + (num * SCALES[counter]);
            counter++;
            if (counter > 2) {
                counter = 0;
            }
        }
        result = result % 10;

        return result;
    }

    private String createCompleteNumberLine(String idNumber, String personalDate, String personalID, String expiryDate, int docCheckNumber, int personalDateCheckNumber, int expiryCheckNumber) {
        return idNumber + docCheckNumber + personalDate + personalDateCheckNumber + expiryDate + expiryCheckNumber + personalID;
    }

    /**
     * Auxiliary class for parsing czech birth number.
     *
     * @author 123030245
     * @see #parse(String)
     * <p>
     * Class imported from APM
     * Removed unused methods
     */
    @Getter
    public class PersonalID {

        /**
         * Regex parsing failed
         */
        private boolean failed;

        /**
         * Gender of PIN
         */
        private int gender = -1;

        /**
         * Date of birth
         */
        private Date birthDate;

        /**
         * Part of PIN after date
         */
        private String secondPart;

        /**
         * internal PIN
         */
        private String internalPIN;

        private static final Pattern PID_PATTERN = Pattern.compile("^(\\d{2})(\\d{2})(\\d{2})/??(\\d{3,4})?$");

        /**
         * Code for male
         */
        private static final int MALE = 0;

        /**
         * Code for female
         */
        private static final int FEMALE = 1;

        private PersonalID() {
        }

        /**
         * Parse personal id string
         *
         * @param personalId
         * @return instance of {@link PersonalID}
         */
        public PersonalID parse(String personalId) {

            // Creates result object
            PersonalID result = new PersonalID();

            // Setup internal PID
            result.internalPIN = personalId;

            Matcher matcher = null;
            // Check if parsing failed
            result.failed = personalId == null || (matcher = PID_PATTERN.matcher(personalId)) == null || matcher.matches() == false;

            // Parse PID
            if (result.failed == false) {

                int year = Integer.parseInt(matcher.group(1));
                int month = Integer.parseInt(matcher.group(2));
                int day = Integer.parseInt(matcher.group(3));

                result.secondPart = matcher.group(4);

                // check if second part isn't empty
                if ("".equals(result.secondPart)) {
                    result.failed = true;
                }
                int length = result.getLength();

                // Correct month and year and set up gender
                if (month > 70 && length == 10) {

                    month -= 70;
                    result.gender = FEMALE;

                } else if (month > 50) {

                    month -= 50;
                    result.gender = FEMALE;

                } else if (month > 20 && length == 10) {

                    month -= 20;
                    result.gender = MALE;

                } else {

                    result.gender = MALE;
                }

                if (year < 54 && length == 10) {
                    year += 2000;
                } else {
                    year += 1900;
                }

                // Create date of birth
                result.birthDate = createDate(year, month, day);
            }

            return result;
        }

        /**
         * Check input parameters and creates specified date
         *
         * @param year
         * @param month
         * @param date
         * @return specified date or null if any parameter is invalid
         */
        private static Date createDate(int year, int month, int date) {

            // init calendar
            Calendar calendar = Calendar.getInstance(new Locale("cs", "CZ"));

            // set calendar to specified date
            calendar.clear();
            calendar.set(year, month - 1, date);

            // Check if specified parameters are corresponding to created date
            if (calendar.get(Calendar.YEAR) != year || calendar.get(Calendar.DATE) != date || calendar.get(Calendar.MONTH) != month - 1) {
                return null;
            }

            return calendar.getTime();
        }


        /**
         * Detects the length of the underlying PID
         *
         * @return length of the PID (treated as a String)
         */
        public int getLength() {
            return internalPIN != null ? internalPIN.replaceAll("/", "").length() : 0;
        }
    }
}
