package com.microsoft.recognizers.text.datetime.parsers;

import com.google.common.collect.ImmutableMap;
import com.microsoft.recognizers.text.ExtractResult;
import com.microsoft.recognizers.text.ParseResult;
import com.microsoft.recognizers.text.datetime.Constants;
import com.microsoft.recognizers.text.datetime.DatePeriodTimexType;
import com.microsoft.recognizers.text.datetime.DateTimeOptions;
import com.microsoft.recognizers.text.datetime.TimeTypeConstants;
import com.microsoft.recognizers.text.datetime.extractors.BaseDateExtractor;
import com.microsoft.recognizers.text.datetime.parsers.config.IDatePeriodParserConfiguration;
import com.microsoft.recognizers.text.datetime.utilities.ConditionalMatch;
import com.microsoft.recognizers.text.datetime.utilities.DateContext;
import com.microsoft.recognizers.text.datetime.utilities.DateTimeFormatUtil;
import com.microsoft.recognizers.text.datetime.utilities.DateTimeResolutionResult;
import com.microsoft.recognizers.text.datetime.utilities.DateUtil;
import com.microsoft.recognizers.text.datetime.utilities.DurationParsingUtil;
import com.microsoft.recognizers.text.datetime.utilities.GetModAndDateResult;
import com.microsoft.recognizers.text.datetime.utilities.NthBusinessDayResult;
import com.microsoft.recognizers.text.datetime.utilities.RegexExtension;
import com.microsoft.recognizers.text.datetime.utilities.TimexUtility;
import com.microsoft.recognizers.text.utilities.IntegerUtility;
import com.microsoft.recognizers.text.utilities.Match;
import com.microsoft.recognizers.text.utilities.MatchGroup;
import com.microsoft.recognizers.text.utilities.RegExpUtility;
import com.microsoft.recognizers.text.utilities.StringUtility;

import java.sql.Time;
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.javatuples.Pair;

public class BaseDatePeriodParser implements IDateTimeParser {

    private static final String parserName = Constants.SYS_DATETIME_DATEPERIOD; //"DatePeriod";
    private static boolean inclusiveEndPeriod = false;

    private final IDatePeriodParserConfiguration config;

    public BaseDatePeriodParser(IDatePeriodParserConfiguration config) {
        this.config = config;
    }

    @Override
    public String getParserName() {
        return parserName;
    }

    @Override
    public ParseResult parse(ExtractResult extractResult) {
        return this.parse(extractResult, LocalDateTime.now());
    }

    @Override
    public DateTimeParseResult parse(ExtractResult er, LocalDateTime refDate) {

        DateTimeResolutionResult value = null;
        if (er.getType().equals(parserName)) {
            DateTimeResolutionResult innerResult = parseBaseDatePeriod(er.getText(), refDate);

            if (!innerResult.getSuccess()) {
                innerResult = parseComplexDatePeriod(er.getText(), refDate);
            }

            if (innerResult.getSuccess()) {
                if (innerResult.getMod() != null && innerResult.getMod().equals(Constants.BEFORE_MOD)) {
                    innerResult.setFutureResolution(ImmutableMap.<String, String>builder()
                            .put(TimeTypeConstants.END_DATE,
                                    DateTimeFormatUtil.formatDate((LocalDateTime)innerResult.getFutureValue()))
                            .build());

                    innerResult.setPastResolution(ImmutableMap.<String, String>builder()
                            .put(TimeTypeConstants.END_DATE,
                                    DateTimeFormatUtil.formatDate((LocalDateTime)innerResult.getPastValue()))
                            .build());
                } else if (innerResult.getMod() != null && innerResult.getMod().equals(Constants.AFTER_MOD)) {
                    innerResult.setFutureResolution(ImmutableMap.<String, String>builder()
                            .put(TimeTypeConstants.START_DATE,
                                    DateTimeFormatUtil.formatDate((LocalDateTime)innerResult.getFutureValue()))
                            .build());

                    innerResult.setPastResolution(ImmutableMap.<String, String>builder()
                            .put(TimeTypeConstants.START_DATE,
                                    DateTimeFormatUtil.formatDate((LocalDateTime)innerResult.getPastValue()))
                            .build());
                } else if (innerResult.getFutureValue() != null && innerResult.getPastValue() != null) {
                    innerResult.setFutureResolution(ImmutableMap.<String, String>builder()
                            .put(TimeTypeConstants.START_DATE,
                                    DateTimeFormatUtil.formatDate(((Pair<LocalDateTime, LocalDateTime>)innerResult.getFutureValue()).getValue0()))
                            .put(TimeTypeConstants.END_DATE,
                                    DateTimeFormatUtil.formatDate(((Pair<LocalDateTime, LocalDateTime>)innerResult.getFutureValue()).getValue1()))
                            .build());

                    innerResult.setPastResolution(ImmutableMap.<String, String>builder()
                            .put(TimeTypeConstants.START_DATE,
                                    DateTimeFormatUtil.formatDate(((Pair<LocalDateTime, LocalDateTime>)innerResult.getPastValue()).getValue0()))
                            .put(TimeTypeConstants.END_DATE,
                                    DateTimeFormatUtil.formatDate(((Pair<LocalDateTime, LocalDateTime>)innerResult.getPastValue()).getValue1()))
                            .build());
                } else {
                    innerResult.setFutureResolution(new HashMap<>());
                    innerResult.setPastResolution(new HashMap<>());
                }
                value = innerResult;
            }
        }

        DateTimeParseResult ret = new DateTimeParseResult(er.getStart(), er.getLength(), er.getText(), er.getType(), er.getData(), value, "", "", er.getMetadata());

        if (value != null) {
            ret.setTimexStr(value.getTimex());
        }

        return ret;
    }

    // Process case like "from|between START to|and END" where START/END can be dateRange or datePoint
    private DateTimeResolutionResult parseComplexDatePeriod(String text, LocalDateTime referenceDate) {
        DateTimeResolutionResult ret = new DateTimeResolutionResult();
        Optional<Match> match = Arrays.stream(RegExpUtility.getMatches(this.config.getComplexDatePeriodRegex(), text)).findFirst();

        if (match.isPresent()) {
            LocalDateTime futureBegin = LocalDateTime.MIN;
            LocalDateTime futureEnd = LocalDateTime.MIN;
            LocalDateTime pastBegin = LocalDateTime.MIN;
            LocalDateTime pastEnd = LocalDateTime.MIN;
            boolean isSpecificDate = false;
            boolean isStartByWeek = false;
            boolean isEndByWeek = false;
            DateContext dateContext = getYearContext(match.get().getGroup("start").value.trim(), match.get().getGroup("end").value.trim(), text);

            DateTimeResolutionResult startResolution = parseSingleTimePoint(match.get().getGroup("start").value.trim(), referenceDate, dateContext);

            if (startResolution.getSuccess()) {
                futureBegin = (LocalDateTime)startResolution.getFutureValue();
                pastBegin = (LocalDateTime)startResolution.getPastValue();
                isSpecificDate = true;
            } else {
                startResolution = parseBaseDatePeriod(match.get().getGroup("start").value.trim(), referenceDate, dateContext);
                if (startResolution.getSuccess()) {
                    futureBegin = ((Pair<LocalDateTime, LocalDateTime>)startResolution.getFutureValue()).getValue0();
                    pastBegin = ((Pair<LocalDateTime, LocalDateTime>)startResolution.getPastValue()).getValue0();

                    if (startResolution.getTimex().contains("-W")) {
                        isStartByWeek = true;
                    }
                }
            }

            if (startResolution.getSuccess()) {
                DateTimeResolutionResult endResolution = parseSingleTimePoint(match.get().getGroup("end").value.trim(), referenceDate, dateContext);

                if (endResolution.getSuccess()) {
                    futureEnd = (LocalDateTime)endResolution.getFutureValue();
                    pastEnd = (LocalDateTime)endResolution.getPastValue();
                    isSpecificDate = true;
                } else {
                    endResolution = parseBaseDatePeriod(match.get().getGroup("end").value.trim(), referenceDate, dateContext);

                    if (endResolution.getSuccess()) {
                        futureEnd = ((Pair<LocalDateTime, LocalDateTime>)endResolution.getFutureValue()).getValue0();
                        pastEnd = ((Pair<LocalDateTime, LocalDateTime>)endResolution.getPastValue()).getValue0();

                        if (endResolution.getTimex().contains("-W")) {
                            isEndByWeek = true;
                        }
                    }
                }

                if (endResolution.getSuccess()) {
                    if (futureBegin.isAfter(futureEnd)) {
                        if (dateContext == null || dateContext.isEmpty()) {
                            futureBegin = pastBegin;
                        } else {
                            futureBegin = dateContext.swiftDateObject(futureBegin, futureEnd);
                        }
                    }

                    if (pastEnd.isBefore(pastBegin)) {
                        if (dateContext == null || dateContext.isEmpty()) {
                            pastEnd = futureEnd;
                        } else {
                            pastBegin = dateContext.swiftDateObject(pastBegin, pastEnd);
                        }
                    }

                    // If both begin/end are date ranges in "Month", the Timex should be ByMonth
                    // The year period case should already be handled in Basic Cases
                    DatePeriodTimexType datePeriodTimexType = DatePeriodTimexType.ByMonth;

                    if (isSpecificDate) {
                        // If at least one of the begin/end is specific date, the Timex should be ByDay
                        datePeriodTimexType = DatePeriodTimexType.ByDay;
                    } else if (isStartByWeek && isEndByWeek) {
                        // If both begin/end are date ranges in "Week", the Timex should be ByWeek
                        datePeriodTimexType = DatePeriodTimexType.ByWeek;
                    }

                    ret.setTimex(TimexUtility.generateDatePeriodTimex(futureBegin, futureEnd, datePeriodTimexType, pastBegin, pastEnd));

                    ret.setFutureValue(new Pair<>(futureBegin, futureEnd));
                    ret.setPastValue(new Pair<>(pastBegin, pastEnd));
                    ret.setSuccess(true);
                }
            }
        }
        return ret;
    }

    private DateTimeResolutionResult parseBaseDatePeriod(String text, LocalDateTime referenceDate) {
        return parseBaseDatePeriod(text, referenceDate, null);
    }

    private DateTimeResolutionResult parseBaseDatePeriod(String text, LocalDateTime referenceDate, DateContext dateContext) {
        DateTimeResolutionResult innerResult = parseMonthWithYear(text, referenceDate);

        if (!innerResult.getSuccess()) {
            innerResult = parseSimpleCases(text, referenceDate);
        }

        if (!innerResult.getSuccess()) {
            innerResult = parseOneWordPeriod(text, referenceDate);
        }

        if (!innerResult.getSuccess()) {
            innerResult = mergeTwoTimePoints(text, referenceDate);
        }

        if (!innerResult.getSuccess()) {
            innerResult = parseYear(text, referenceDate);
        }

        if (!innerResult.getSuccess()) {
            innerResult = parseWeekOfMonth(text, referenceDate);
        }

        if (!innerResult.getSuccess()) {
            innerResult = parseWeekOfYear(text, referenceDate);
        }

        if (!innerResult.getSuccess()) {
            innerResult = parseHalfYear(text, referenceDate);
        }

        if (!innerResult.getSuccess()) {
            innerResult = parseQuarter(text, referenceDate);
        }

        if (!innerResult.getSuccess()) {
            innerResult = parseSeason(text, referenceDate);
        }

        if (!innerResult.getSuccess()) {
            innerResult = parseWhichWeek(text, referenceDate);
        }

        if (!innerResult.getSuccess()) {
            innerResult = parseWeekOfDate(text, referenceDate);
        }

        if (!innerResult.getSuccess()) {
            innerResult = parseMonthOfDate(text, referenceDate);
        }

        if (!innerResult.getSuccess()) {
            innerResult = parseDecade(text, referenceDate);
        }

        // Cases like "within/less than/more than x weeks from/before/after today"
        if (!innerResult.getSuccess()) {
            innerResult = parseDatePointWithAgoAndLater(text, referenceDate);
        }

        // Parse duration should be at the end since it will extract "the last week" from "the last week of July"
        if (!innerResult.getSuccess()) {
            innerResult = parseDuration(text, referenceDate);
        }

        // Cases like "21st century"
        if (!innerResult.getSuccess()) {
            innerResult = parseOrdinalNumberWithCenturySuffix(text, referenceDate);
        }

        if (innerResult.getSuccess() && dateContext != null) {
            innerResult = dateContext.processDatePeriodEntityResolution(innerResult);
        }

        return innerResult;
    }

    private DateTimeResolutionResult parseOrdinalNumberWithCenturySuffix(String text, LocalDateTime referenceDate) {

        DateTimeResolutionResult ret = new DateTimeResolutionResult();
        Optional<ExtractResult> er = this.config.getOrdinalExtractor().extract(text).stream().findFirst();

        if (er.isPresent() && er.get().getStart() + er.get().getLength() < text.length()) {
            String afterString = text.substring(er.get().getStart() + er.get().getLength()).trim();

            // It falls into the cases like "21st century"
            if (Arrays.stream(RegExpUtility.getMatches(this.config.getCenturySuffixRegex(), afterString)).findFirst().isPresent()) {
                ParseResult number = this.config.getNumberParser().parse(er.get());

                if (number.getValue() != null) {
                    // Note that 1st century means from year 0 - 100
                    int startYear = (Math.round(((Double)number.getValue()).floatValue()) - 1) * Constants.CenturyYearsCount;
                    LocalDateTime startDate = DateUtil.safeCreateFromMinValue(startYear, 1, 1);
                    LocalDateTime endDate = DateUtil.safeCreateFromMinValue(startYear + Constants.CenturyYearsCount, 1, 1);

                    String startLuisStr = DateTimeFormatUtil.luisDate(startDate);
                    String endLuisStr = DateTimeFormatUtil.luisDate(endDate);
                    String durationTimex = "P" + Constants.CenturyYearsCount + "Y";

                    ret.setTimex(String.format("(%s,%s,%s)", startLuisStr, endLuisStr, durationTimex));
                    ret.setFutureValue(new Pair<>(startDate, endDate));
                    ret.setPastValue(new Pair<>(startDate, endDate));
                    ret.setSuccess(true);
                }
            }
        }

        return ret;
    }

    private DateTimeResolutionResult parseDatePointWithAgoAndLater(String text, LocalDateTime referenceDate) {

        DateTimeResolutionResult ret = new DateTimeResolutionResult();
        Optional<ExtractResult> er = this.config.getDateExtractor().extract(text, referenceDate).stream().findFirst();

        if (er.isPresent()) {
            String beforeString = text.substring(0, er.get().getStart());
            boolean isAgo = Arrays.stream(RegExpUtility.getMatches(this.config.getAgoRegex(), er.get().getText())).findFirst().isPresent();
            boolean isLater = Arrays.stream(RegExpUtility.getMatches(this.config.getLaterRegex(), er.get().getText())).findFirst().isPresent();

            if (!StringUtility.isNullOrEmpty(beforeString) && (isAgo || isLater)) {
                boolean isLessThanOrWithIn = false;
                boolean isMoreThan = false;

                // cases like "within 3 days from yesterday/tomorrow" does not make any sense
                if (er.get().getText().contains("today") || er.get().getText().contains("now")) {
                    Optional<Match> match = Arrays.stream(RegExpUtility.getMatches(this.config.getWithinNextPrefixRegex(), beforeString)).findFirst();
                    if (match.isPresent()) {
                        boolean isNext = !StringUtility.isNullOrEmpty(match.get().getGroup("next").value);

                        // cases like "within the next 5 days before today" is not acceptable
                        if (!(isNext && isAgo)) {
                            isLessThanOrWithIn = true;
                        }
                    }
                }

                isLessThanOrWithIn = isLessThanOrWithIn || (Arrays.stream(RegExpUtility.getMatches(this.config.getLessThanRegex(), beforeString)).findFirst().isPresent());
                isMoreThan = Arrays.stream(RegExpUtility.getMatches(this.config.getMoreThanRegex(), beforeString)).findFirst().isPresent();

                DateTimeParseResult pr = this.config.getDateParser().parse(er.get(), referenceDate);
                Optional<ExtractResult> durationExtractionResult = this.config.getDurationExtractor().extract(er.get().getText()).stream().findFirst();

                if (durationExtractionResult.isPresent()) {
                    ParseResult duration = this.config.getDurationParser().parse(durationExtractionResult.get());
                    long durationInSeconds = Math.round((Double)((DateTimeResolutionResult)(duration.getValue())).getPastValue());

                    if (isLessThanOrWithIn) {
                        LocalDateTime startDate;
                        LocalDateTime endDate;

                        if (isAgo) {
                            startDate = (LocalDateTime)((DateTimeResolutionResult)(pr.getValue())).getPastValue();
                            endDate = startDate.plusSeconds(durationInSeconds);
                        } else {
                            endDate = (LocalDateTime)((DateTimeResolutionResult)(pr.getValue())).getFutureValue();
                            startDate = endDate.minusSeconds(durationInSeconds);
                        }

                        if (startDate != LocalDateTime.MIN) {
                            String startLuisStr = DateTimeFormatUtil.luisDate(startDate);
                            String endLuisStr = DateTimeFormatUtil.luisDate(endDate);
                            String durationTimex = ((DateTimeResolutionResult)(duration.getValue())).getTimex();

                            ret.setTimex(String.format("(%s,%s,%s)", startLuisStr, endLuisStr, durationTimex));
                            ret.setFutureValue(new Pair<>(startDate, endDate));
                            ret.setPastValue(new Pair<>(startDate, endDate));
                            ret.setSuccess(true);
                        }
                    } else if (isMoreThan) {
                        ret.setMod(isAgo ? Constants.BEFORE_MOD : Constants.AFTER_MOD);

                        ret.setTimex(pr.getTimexStr());
                        ret.setFutureValue(((DateTimeResolutionResult)(pr.getValue())).getFutureValue());
                        ret.setPastValue(((DateTimeResolutionResult)(pr.getValue())).getPastValue());
                        ret.setSuccess(true);
                    }
                }
            }
        }

        return ret;
    }

    private DateTimeResolutionResult parseSingleTimePoint(String text, LocalDateTime referenceDate, DateContext dateContext) {

        DateTimeResolutionResult ret = new DateTimeResolutionResult();
        ExtractResult er = this.config.getDateExtractor().extract(text, referenceDate).stream().findFirst().orElse(null);

        if (er != null) {
            Optional<Match> match = Arrays.stream(RegExpUtility.getMatches(this.config.getWeekWithWeekDayRangeRegex(), text)).findFirst();
            String weekPrefix = null;
            if (match.isPresent()) {
                weekPrefix = match.get().getGroup("week").value;
            }

            if (!StringUtility.isNullOrEmpty(weekPrefix)) {
                er.setText(weekPrefix + " " + er.getText());
            }

            ParseResult pr = this.config.getDateParser().parse(er, referenceDate);

            if (pr != null) {
                ret.setTimex("(" + ((DateTimeParseResult)pr).getTimexStr());
                ret.setFutureValue(((DateTimeResolutionResult)pr.getValue()).getFutureValue());
                ret.setPastValue(((DateTimeResolutionResult)pr.getValue()).getPastValue());
                ret.setSuccess(true);
            }

            if (dateContext != null) {
                ret = dateContext.processDateEntityResolution(ret);
            }
        }

        return ret;
    }

    private DateTimeResolutionResult parseSimpleCases(String text, LocalDateTime referenceDate) {

        DateTimeResolutionResult ret = new DateTimeResolutionResult();
        int year = referenceDate.getYear();
        int month = referenceDate.getMonthValue();
        int beginDay;
        int endDay;
        boolean noYear = true;

        ConditionalMatch match = RegexExtension.matchExact(this.config.getMonthFrontBetweenRegex(), text, true);
        String beginLuisStr;
        String endLuisStr;

        if (!match.getSuccess()) {
            match = RegexExtension.matchExact(this.config.getBetweenRegex(), text, true);
        }

        if (!match.getSuccess()) {
            match = RegexExtension.matchExact(this.config.getMonthFrontSimpleCasesRegex(), text, true);
        }

        if (!match.getSuccess()) {
            match = RegexExtension.matchExact(this.config.getSimpleCasesRegex(), text, true);
        }

        if (match.getSuccess()) {
            MatchGroup days = match.getMatch().get().getGroup("day");
            beginDay = this.config.getDayOfMonth().get(days.captures[0].value.toLowerCase());
            endDay = this.config.getDayOfMonth().get(days.captures[1].value.toLowerCase());

            // parse year
            year = ((BaseDateExtractor)this.config.getDateExtractor()).getYearFromText(match.getMatch().get());
            if (year != Constants.InvalidYear) {
                noYear = false;
            } else {
                year = referenceDate.getYear();
            }

            String monthStr = match.getMatch().get().getGroup("month").value;
            if (!StringUtility.isNullOrEmpty(monthStr)) {
                month = this.config.getMonthOfYear().get(monthStr.toLowerCase());
            } else {
                monthStr = match.getMatch().get().getGroup("relmonth").value.trim().toLowerCase();
                int swiftMonth = this.config.getSwiftDayOrMonth(monthStr);
                switch (swiftMonth) {
                    case 1:
                        if (month != 12) {
                            month += 1;
                        } else {
                            month = 1;
                            year += 1;
                        }
                        break;
                    case -1:
                        if (month != 1) {
                            month -= 1;
                        } else {
                            month = 12;
                            year -= 1;
                        }
                        break;
                    default:
                        break;
                }

                if (this.config.isFuture(monthStr)) {
                    noYear = false;
                }
            }
        } else {
            return ret;
        }

        if (noYear) {
            beginLuisStr = DateTimeFormatUtil.luisDate(-1, month, beginDay);
            endLuisStr = DateTimeFormatUtil.luisDate(-1, month, endDay);
        } else {
            beginLuisStr = DateTimeFormatUtil.luisDate(year, month, beginDay);
            endLuisStr = DateTimeFormatUtil.luisDate(year, month, endDay);
        }

        int futureYear = year;
        int pastYear = year;
        LocalDateTime startDate = DateUtil.safeCreateFromMinValue(year, month, beginDay);

        if (noYear && startDate.isBefore(referenceDate)) {
            futureYear++;
        }

        if (noYear && (startDate.isAfter(referenceDate) || startDate.isEqual(referenceDate))) {
            pastYear--;
        }

        HashMap<String, LocalDateTime> futurePastBeginDates = DateContext.generateDates(noYear, referenceDate, year, month, beginDay);
        HashMap<String, LocalDateTime> futurePastEndDates = DateContext.generateDates(noYear, referenceDate, year, month, endDay);

        ret.setTimex(String.format("(%s,%s,P%sD)", beginLuisStr, endLuisStr, (endDay - beginDay)));
        ret.setFutureValue(new Pair<>(futurePastBeginDates.get(Constants.FutureDate),
                futurePastEndDates.get(Constants.FutureDate)));
        ret.setPastValue(new Pair<>(futurePastBeginDates.get(Constants.PastDate),
                futurePastEndDates.get(Constants.PastDate)));
        ret.setSuccess(true);

        return ret;
    }

    private boolean isPresent(int swift) {
        return swift == 0;
    }

    private DateTimeResolutionResult parseOneWordPeriod(String text, LocalDateTime referenceDate) {

        DateTimeResolutionResult ret = new DateTimeResolutionResult();
        int year = referenceDate.getYear();
        int month = referenceDate.getMonthValue();
        int futureYear = year;
        int pastYear = year;
        boolean earlyPrefix = false;
        boolean latePrefix = false;
        boolean midPrefix = false;
        boolean isRef = false;

        boolean earlierPrefix = false;
        boolean laterPrefix = false;

        String trimmedText = text.trim().toLowerCase();
        ConditionalMatch match = RegexExtension.matchExact(this.config.getOneWordPeriodRegex(), trimmedText, true);

        if (!match.getSuccess()) {
            match = RegexExtension.matchExact(this.config.getLaterEarlyPeriodRegex(), trimmedText, true);
        }

        // For cases "that week|month|year"
        if (!match.getSuccess()) {
            match = RegexExtension.matchExact(this.config.getReferenceDatePeriodRegex(), trimmedText, true);
            isRef = true;
            ret.setMod(Constants.REF_UNDEF_MOD);
        }

        if (match.getSuccess()) {
            if (!match.getMatch().get().getGroup("EarlyPrefix").value.equals("")) {
                earlyPrefix = true;
                trimmedText = match.getMatch().get().getGroup(Constants.SuffixGroupName).value;
                ret.setMod(Constants.EARLY_MOD);
            } else if (!match.getMatch().get().getGroup("LatePrefix").value.equals("")) {
                latePrefix = true;
                trimmedText = match.getMatch().get().getGroup(Constants.SuffixGroupName).value;
                ret.setMod(Constants.LATE_MOD);
            } else if (!match.getMatch().get().getGroup("MidPrefix").value.equals("")) {
                midPrefix = true;
                trimmedText = match.getMatch().get().getGroup(Constants.SuffixGroupName).value;
                ret.setMod(Constants.MID_MOD);
            }

            int swift = 0;
            String monthStr = match.getMatch().get().getGroup("month").value;
            if (!StringUtility.isNullOrEmpty(monthStr)) {
                swift = this.config.getSwiftYear(trimmedText);
            } else {
                swift = this.config.getSwiftDayOrMonth(trimmedText);
            }

            // Handle the abbreviation of DatePeriod, e.g., 'eoy(end of year)', the behavior of 'eoy' should be the same as 'end of year'
            Optional<Match> unspecificEndOfRangeMatch = Arrays.stream(RegExpUtility.getMatches(config.getUnspecificEndOfRangeRegex(), match.getMatch().get().value)).findFirst();
            if (unspecificEndOfRangeMatch.isPresent()) {
                latePrefix = true;
                trimmedText = match.getMatch().get().value;
                ret.setMod(Constants.LATE_MOD);
            }

            if (!match.getMatch().get().getGroup("RelEarly").value.equals("")) {
                earlierPrefix = true;
                if (isPresent(swift)) {
                    ret.setMod(null);
                }
            } else if (!match.getMatch().get().getGroup("RelLate").value.equals("")) {
                laterPrefix = true;
                if (isPresent(swift)) {
                    ret.setMod(null);
                }
            }

            if (this.config.isYearToDate(trimmedText)) {
                ret.setTimex(String.format("%04d", referenceDate.getYear()));
                ret.setFutureValue(new Pair<>(
                        DateUtil.safeCreateFromMinValue(referenceDate.getYear(), 1, 1), referenceDate));
                ret.setPastValue(new Pair<>(
                        DateUtil.safeCreateFromMinValue(referenceDate.getYear(), 1, 1), referenceDate));

                ret.setSuccess(true);
                return ret;
            }

            if (this.config.isMonthToDate(trimmedText)) {
                ret.setTimex(String.format("%04d-%02d", referenceDate.getYear(), referenceDate.getMonthValue()));
                ret.setFutureValue(new Pair<>(
                        DateUtil.safeCreateFromMinValue(referenceDate.getYear(), referenceDate.getMonthValue(), 1),
                        referenceDate));
                ret.setPastValue(new Pair<>(
                        DateUtil.safeCreateFromMinValue(referenceDate.getYear(), referenceDate.getMonthValue(), 1),
                        referenceDate));

                ret.setSuccess(true);
                return ret;
            }

            if (!StringUtility.isNullOrEmpty(monthStr)) {
                swift = this.config.getSwiftYear(trimmedText);

                month = this.config.getMonthOfYear().get(monthStr.toLowerCase());

                if (swift >= -1) {
                    ret.setTimex(String.format("%04d-%02d", referenceDate.getYear() + swift, month));
                    year = year + swift;
                    futureYear = pastYear = year;
                } else {
                    ret.setTimex(String.format("XXXX-%02d", month));
                    if (month < referenceDate.getMonthValue()) {
                        futureYear++;
                    }

                    if (month >= referenceDate.getMonthValue()) {
                        pastYear--;
                    }
                }
            } else {
                swift = this.config.getSwiftDayOrMonth(trimmedText);

                if (this.config.isWeekOnly(trimmedText)) {
                    LocalDateTime thursday = DateUtil.thisDate(referenceDate, DayOfWeek.THURSDAY.getValue()).plusDays(Constants.WeekDayCount * swift);

                    ret.setTimex(isRef ? TimexUtility.generateWeekTimex() : TimexUtility.generateWeekTimex(thursday));

                    LocalDateTime beginDate = DateUtil.thisDate(referenceDate, DayOfWeek.MONDAY.getValue()).plusDays(Constants.WeekDayCount * swift);

                    LocalDateTime endValue = DateUtil.thisDate(referenceDate, DayOfWeek.SUNDAY.getValue()).plusDays(Constants.WeekDayCount * swift);

                    LocalDateTime endDate = inclusiveEndPeriod ? endValue : endValue.plusDays(1);

                    if (earlyPrefix) {
                        endValue = DateUtil.thisDate(referenceDate, DayOfWeek.WEDNESDAY.getValue()).plusDays(Constants.WeekDayCount * swift);
                        endDate = inclusiveEndPeriod ? endValue : endValue.plusDays(1);
                    } else if (midPrefix) {
                        beginDate = DateUtil.thisDate(referenceDate, DayOfWeek.TUESDAY.getValue()).plusDays(Constants.WeekDayCount * swift);
                        endValue = DateUtil.thisDate(referenceDate, DayOfWeek.FRIDAY.getValue()).plusDays(Constants.WeekDayCount * swift);
                        endDate = inclusiveEndPeriod ? endValue : endValue.plusDays(1);
                    } else if (latePrefix) {
                        beginDate = DateUtil.thisDate(referenceDate, DayOfWeek.THURSDAY.getValue()).plusDays(Constants.WeekDayCount * swift);
                    }

