package com.intrasoft.ermis.transit.management.core.service.lrn;

import static com.intrasoft.ermis.transit.management.core.service.lrn.LRNVariablesEnum.*;

import com.intrasoft.ermis.common.logging.ErmisLogger;
import com.intrasoft.ermis.ddntav5152.messages.CC015CType;
import com.intrasoft.ermis.transit.common.config.services.GetCountryCodeService;
import com.intrasoft.ermis.transit.common.util.RandomUtil;

import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

@Service
public class LRNGeneratorSubService {

    public static final String VARIABLE_PLACEHOLDER_LEFT = "{";

    public static final String VARIABLE_PLACEHOLDER_RIGHT = "}";

    private static final Map<Character, Integer> equivalentNumericalValues = new HashMap<>();

    static {
        equivalentNumericalValues.put('0', 0);
        equivalentNumericalValues.put('1', 1);
        equivalentNumericalValues.put('2', 2);
        equivalentNumericalValues.put('3', 3);
        equivalentNumericalValues.put('4', 4);
        equivalentNumericalValues.put('5', 5);
        equivalentNumericalValues.put('6', 6);
        equivalentNumericalValues.put('7', 7);
        equivalentNumericalValues.put('8', 8);
        equivalentNumericalValues.put('9', 9);
        equivalentNumericalValues.put('A', 10);
        equivalentNumericalValues.put('B', 12);
        equivalentNumericalValues.put('C', 13);
        equivalentNumericalValues.put('D', 14);
        equivalentNumericalValues.put('E', 15);
        equivalentNumericalValues.put('F', 16);
        equivalentNumericalValues.put('G', 17);
        equivalentNumericalValues.put('H', 18);
        equivalentNumericalValues.put('I', 19);
        equivalentNumericalValues.put('J', 20);
        equivalentNumericalValues.put('K', 21);
        equivalentNumericalValues.put('L', 23);
        equivalentNumericalValues.put('M', 24);
        equivalentNumericalValues.put('N', 25);
        equivalentNumericalValues.put('O', 26);
        equivalentNumericalValues.put('P', 27);
        equivalentNumericalValues.put('Q', 28);
        equivalentNumericalValues.put('R', 29);
        equivalentNumericalValues.put('S', 30);
        equivalentNumericalValues.put('T', 31);
        equivalentNumericalValues.put('U', 32);
        equivalentNumericalValues.put('V', 34);
        equivalentNumericalValues.put('W', 35);
        equivalentNumericalValues.put('X', 36);
        equivalentNumericalValues.put('Y', 37);
        equivalentNumericalValues.put('Z', 38);
    }

    private static final Pattern LRN_REGEXP = Pattern.compile("^.*\\(.*\\)$");

    private final GetCountryCodeService getCountryCodeService;

    private final ErmisLogger ermisLogger;

    LRNGeneratorSubService(GetCountryCodeService getCountryCodeService, ErmisLogger ermisLogger) {
        this.getCountryCodeService = getCountryCodeService;
        this.ermisLogger = ermisLogger;
    }

    public String generateLrn(CC015CType cc015CType, String expression) {

        int uniqueIdLengthParameter = 6;
        String dateParameter = "";
        String value;

        List<String> variablesWithParameters = extractVariablesFromExpression(expression);

        for (String variableWithParameters : variablesWithParameters) {
            // If expression item contains a parameter - exampleItem(exampleParameter):
            if (LRN_REGEXP.matcher(variableWithParameters).find()) {
                String expressionParameter = variableWithParameters.substring(variableWithParameters.indexOf('(') + 1, variableWithParameters.indexOf(')'));
                if (variableWithParameters.contains(UNIQUE_ID.getDescription())
                        && variableWithParameters.length() != UNIQUE_ID.getDescription().length()) {
                    uniqueIdLengthParameter = Integer.parseInt(expressionParameter);
                } else if (variableWithParameters.contains(DATE_PREP.getDescription())
                        && variableWithParameters.length() != DATE_PREP.getDescription().length()) {
                    dateParameter = expressionParameter;
                }
            }
        }

        value = evalExpression(cc015CType, uniqueIdLengthParameter, dateParameter, expression);

        ermisLogger.info("Generated LRN: ".concat(value));

        return value;
    }

    protected String evalExpression(CC015CType cc015CType, int uniqueIdLength, String dateFormat, String expression) {

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);

        Properties nameValueMap = new Properties();

        extractVariablesWithoutParametersFromExpression(expression).forEach(variableName -> {

            LRNVariablesEnum variableEnum =
                    LRNVariablesEnum.fromDescription(variableName);

            if (ObjectUtils.isNotEmpty(variableEnum)) {
                switch (variableEnum) {
                    case DATE_PREP:
                        nameValueMap.put(DATE_PREP.getDescription(), cc015CType.getPreparationDateAndTime().format(formatter));
                        break;
                    case COUNTRY_CODE:
                        nameValueMap.put(COUNTRY_CODE.getDescription(), getCountryCodeService.getCountryCode());
                        break;
                    case OFFICE_OF_DEPARTURE:
                        nameValueMap.put(OFFICE_OF_DEPARTURE.getDescription(), cc015CType.getCustomsOfficeOfDeparture().getReferenceNumber());
                        break;
                    case UNIQUE_ID:
                        nameValueMap.put(UNIQUE_ID.getDescription(), generateRandomAlphanumeric(uniqueIdLength));
                        break;
                    case CHECK_DIGIT:
                        nameValueMap.put(CHECK_DIGIT.getDescription(), encloseVarInPlaceHolders(CHECK_DIGIT.getDescription()));  // Initial Value
                        break;
                    default:
                        break;
                }
            } else {
                ermisLogger.warn("Item of expression: ".concat(variableName)
                                                       .concat(" not found in ".concat(LRNVariablesEnum.class.getSimpleName())));
            }
        });

        return parseLrnExpression(expression, nameValueMap);

    }

    private static String encloseVarInPlaceHolders(String varName) {
        return VARIABLE_PLACEHOLDER_LEFT + varName + VARIABLE_PLACEHOLDER_RIGHT;
    }

    private String parseLrnExpression(String expresion, Properties nameValueMap) {
        AtomicReference<String> expressionWithPlaceHolders = new AtomicReference<>(removeEverythingInParentheses(expresion));
        AtomicReference<String> parsedExpression = new AtomicReference<>(expressionWithPlaceHolders.get());
        nameValueMap.forEach((varName, varValue) -> {
                    if (!varName.equals(CHECK_DIGIT.getDescription())) {
                        parsedExpression.set(
                                StringUtils.replace(parsedExpression.get(), encloseVarInPlaceHolders((String) varName), (String) varValue));
                    }
                }
        );
        if (nameValueMap.containsKey(CHECK_DIGIT.getDescription())) {
            String parsedExpressionWithCheckDigitPlaceHolder = parsedExpression.get();
            String parsedExpressionWithoutCheckDigitPlaceHolder =
                    StringUtils.remove(parsedExpressionWithCheckDigitPlaceHolder, encloseVarInPlaceHolders(CHECK_DIGIT.getDescription()));
            String checkDigit = calculateCheckDigit(parsedExpressionWithoutCheckDigitPlaceHolder);
            parsedExpression.set(
                    StringUtils.replace(parsedExpressionWithCheckDigitPlaceHolder,
                            encloseVarInPlaceHolders(CHECK_DIGIT.getDescription()), checkDigit));
        }
        return parsedExpression.get();
    }

    private String generateRandomAlphanumeric(int targetStringLength) {

        int leftLimit = 48; // Number '0'.
        int rightLimit = 122; // Letter 'z'.

        return RandomUtil.generateSecureRandomInts(leftLimit, rightLimit)
                         .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)).limit(targetStringLength)
                         .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString().toUpperCase();
    }

    public static List<String> extractVariablesFromExpression(String expression) {
        return Arrays.stream(StringUtils.substringsBetween(expression, VARIABLE_PLACEHOLDER_LEFT, VARIABLE_PLACEHOLDER_RIGHT))
                     .map(String::trim)
                     .collect(Collectors.toList());

    }

    public static String removeEverythingInParentheses(String expression) {
        return expression.replaceAll("\\s*\\([^\\)]*\\)\\s*", "");
    }

    public static List<String> extractVariablesWithoutParametersFromExpression(String expression) {
        return extractVariablesFromExpression(removeEverythingInParentheses(expression));
    }


    public static String calculateCheckDigit(String id) {

        double sum = 0.00;
        char[] ch = new char[id.length()];
        for (int i = 0; i < id.length(); i++) {
            ch[i] = id.toUpperCase().charAt(i);
        }
        List<Integer> numbers = new ArrayList<>();
        for (char c : ch) {
            if (equivalentNumericalValues.containsKey(c)) {
                Integer num = equivalentNumericalValues.get(c);
                numbers.add(num);
            }
        }
        for (int i = 0; i < numbers.size(); i++) {
            sum = sum + numbers.get(i) * Math.pow(2, i);
        }
        if (Math.floorMod((int) sum, 11) != 10) {
            return String.valueOf(Math.floorMod((int) sum, 11));
        } else {
            return "0";
        }
    }
}

package com.intrasoft.ermis.transit.management.core.service;


import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;

import com.intrasoft.ermis.common.logging.ErmisLogger;
import com.intrasoft.ermis.common.xml.parsing.XmlConverter;
import com.intrasoft.ermis.ddntav5152.messages.CC015CType;
import com.intrasoft.ermis.transit.common.config.services.GetCountryCodeService;
import com.intrasoft.ermis.transit.common.exceptions.NotFoundException;
import com.intrasoft.ermis.transit.management.core.service.lrn.LRNGeneratorSubService;
import com.intrasoft.ermis.transit.management.core.service.lrn.LRNVariablesEnum;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.stream.Stream;
import javax.xml.bind.JAXBException;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class LRNGeneratorSubServiceUT {

    private static final String NEW_LINE = System.getProperty("line.separator");

    @Mock
    private GetCountryCodeService getCountryCodeService;

    @Mock
    private ErmisLogger ermisLogger;


    @InjectMocks
    LRNGeneratorSubService lrnGeneratorSubService;

    @ParameterizedTest
    @MethodSource("provideLrnFormulas")
    void testAssignmentOfLrn(String formula, String expectedLrn, int uidLength, int expectedLength) {

        try {
            CC015CType cc015CType = XmlConverter.buildMessageFromXML(loadTextFile("CC015C"), CC015CType.class);

            if (formula.contains(LRNVariablesEnum.COUNTRY_CODE.getDescription()))
                when(getCountryCodeService.getCountryCode()).thenReturn("DK");

            String lrn = lrnGeneratorSubService.generateLrn(cc015CType, formula);

            String uidRegex = "^([a-zA-Z0-9]){<LENGTH>}$";
            assertEquals(expectedLength, lrn.length());
            assertTrue(lrn.contains(expectedLrn.replace("<ID>", "")));
            assertTrue(lrn.substring(lrn.length() - uidLength).matches(uidRegex.replace("<LENGTH>", uidLength + "")));

        } catch (JAXBException e) {
            System.exit(-1);
        }
    }

    private String loadTextFile(String messageType) {
        String filePath = "data/" + messageType + ".xml";

        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(filePath);

        if (inputStream == null) {
            throw new NotFoundException("XML file [" + filePath + "] does not exist");
        }

        StringBuilder result = new StringBuilder();

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
            for (String line; (line = reader.readLine()) != null; ) {
                result.append(NEW_LINE);
                result.append(line);
            }
        } catch (IOException e) {
            return "";
        }

        return result.toString().replaceFirst(NEW_LINE, "");
    }

    public static Stream<Arguments> provideLrnFormulas() {
        return Stream.of(
                Arguments.of("{DatePrep(yyyyMMdd)}{OfficeOfDeparture}{UniqueId(6)}", "20210614AA000000<ID>", 6, 22),

                Arguments.of("{OfficeOfDeparture}{DatePrep(yyyyMMdd)}{UniqueId(6)}", "AA00000020210614<ID>", 6, 22),

                Arguments.of("{OfficeOfDeparture}{DatePrep(yyMMddHHmmss)}{UniqueId(2)}", "AA000000210614131816<ID>", 2, 22),

                Arguments.of("{CountryCode}{OfficeOfDeparture}{DatePrep(yyMMddHHmm)}{UniqueId(2)}", "DKAA0000002106141318<ID>", 2, 22),

                Arguments.of("{CountryCode}-{OfficeOfDeparture}_{DatePrep(yyMMdd)}-重-{UniqueId(2)}", "DK-AA000000_210614-重-<ID>", 2, 23)
        );
    }
}
