package com.capgemini.reports.periodicity;

import com.capgemini.reports.database.javabean.DbSchedulingCriteriaVal;
import com.capgemini.reports.database.javabean.enumeration.SchedulingEnum;
import com.capgemini.reports.exceptions.InvalidInputException;
import com.capgemini.reports.exceptions.InvalidUserInputException;

import java.text.ParseException;
import java.util.Calendar;
import java.util.List;

/** Factory class to easily build the various types of scheduling. */
public final class PeriodFactory {

    private PeriodFactory() {
        // Nothing to do
    }

    /**
     * Builds a {@link SchedulingPeriod} that occurs once every week.
     * 
     * @param dayInWeek the day of the week.
     * @return a {@link SchedulingPeriod}
     */
    public static WeeklyPeriod weekly(final int dayInWeek) {
        return new WeeklyPeriod(dayInWeek);
    }

    /**
     * Builds a {@link SchedulingPeriod} that occurs every day.
     * 
     * @return a {@link SchedulingPeriod}
     */
    public static DailyPeriod daily() {
        return DailyPeriod.getInstance();
    }

    /**
     * Builds a {@link SchedulingPeriod} that occurs once, on a specific day.
     * 
     * @param day the day of the execution.
     * @return a {@link SchedulingPeriod}
     * @throws ParseException if unable to parse the String date
     */
    public static SpecificDay day(final String day) throws ParseException {
        return new SpecificDay(day);
    }

    /**
     * Builds a {@link SchedulingPeriod} that occurs once every month.
     * 
     * @param dayInMonth the day of the month.
     * @return a {@link SchedulingPeriod}
     */
    public static DayInMonth dayInMonth(final int dayInMonth) {
        return new DayInMonth(dayInMonth);
    }

    /**
     * Builds a {@link SchedulingPeriod} that occurs once every month, on a weekend.
     * 
     * @param weekend the weekend.
     * @return a {@link SchedulingPeriod}
     */
    public static WeekendPeriod weekend(final String weekend) {
        return WeekendPeriod.getEnum(weekend);
    }

    /**
     * Builds a {@link SchedulingPeriod} out of a series of Db entries (@link SchedulingPeriod}s).
     * 
     * @param criteria a {@link List} of {@link DbSchedulingCriteriaVal} obtained from the DB
     * @return the {@link SchedulingEnum}
     * @throws InvalidInputException if unable to construct the period.
     */
    public static SchedulingPeriod fromDbSchedulingCriteria(final List<DbSchedulingCriteriaVal> criteria) throws InvalidInputException {
        // First find periodicity type (date / daily / weekly / monthly / weekend / day_in_month)
        SchedulingEnum periodicity = getPeriodicity(criteria);
        // Now findAll the value defining the periodicity
        String value = getPeriodicityValue(criteria, periodicity);
        return fromPeriodicityArguments(periodicity, value);
    }

    private static String getPeriodicityValue(final List<DbSchedulingCriteriaVal> criteria, final SchedulingEnum periodicity)
            throws InvalidInputException {
        if (periodicity == SchedulingEnum.DAILY) {
            return ""; // No value for daily periodicity
        }
        for (DbSchedulingCriteriaVal criterion : criteria) {
            final String criteriaName = criterion.getDbCriteria().getCriteriaName();
            if (periodicity.getCriteriaName().equals(criteriaName)) {
                return criterion.getCriteriaValue();
            }
        }
        throw new InvalidInputException("Unable to parse criteria for periodicity value : " + criteria.toString());
    }

    private static SchedulingEnum getBasicPeriodicity(final List<DbSchedulingCriteriaVal> criteria) throws InvalidInputException {
        for (DbSchedulingCriteriaVal criterion : criteria) {
            final String criteriaName = criterion.getDbCriteria().getCriteriaName();
            if (SchedulingEnum.PERIOD.toString().equals(criteriaName)) {
                final String periodValue = criterion.getCriteriaValue().toLowerCase();
                return SchedulingEnum.getEnum(periodValue);
            }
        }
        throw new InvalidInputException("Unable to parse criteria for periodicity type : " + criteria.toString());
    }

    private static SchedulingEnum getPeriodicity(final List<DbSchedulingCriteriaVal> criteria) throws InvalidInputException {
        SchedulingEnum periodicity = getBasicPeriodicity(criteria);
        if (periodicity == SchedulingEnum.WEEKEND) {
            // Special periodicity type, might be constructed in three tiers:
            // - period = weekend
            // - weekend = day_in_month
            // - day_in_month_value = [1..31]
            // Instead of usual two-tiers:
            // - period = weekend
            // - weekend = first_we
            String value = getPeriodicityValue(criteria, SchedulingEnum.WEEKEND);
            if ("day_in_month_value".equals(value)) {
                return SchedulingEnum.DAY_IN_MONTH_VALUE;
            }
        }
        // Normal mode, periodicity was correctly found in 1st iteration
        return periodicity;
    }

    private static SchedulingPeriod fromPeriodicityArguments(final SchedulingEnum periodicityEnum, final String periodValue)
            throws InvalidInputException {
        SchedulingPeriod thePeriod;
        switch (periodicityEnum) {
            case DAILY:
                thePeriod = PeriodFactory.daily();
                break;
            case DATE:
                try {
                    thePeriod = PeriodFactory.day(periodValue);
                } catch (ParseException e) {
                    throw new InvalidUserInputException("Unable to parse date: " + periodValue, e);
                }
                break;
            case DAY_IN_MONTH_VALUE:
                try {
                    final int dayInMonth = Integer.parseInt(periodValue);
                    thePeriod = PeriodFactory.dayInMonth(dayInMonth);
                } catch (NumberFormatException e) {
                    throw new InvalidUserInputException("Unable to parse day of month number (int): " + periodValue, e);
                }
                break;
            case WEEKEND:
                thePeriod = PeriodFactory.weekend(periodValue);
                break;
            case WEEKLY:
                try {
                    final int dayInWeek = Integer.parseInt(periodValue);
                    thePeriod = PeriodFactory.weekly(dayInWeek);
                } catch (NumberFormatException e) {
                    throw new InvalidUserInputException("Unable to parse day of week number (int): " + periodValue, e);
                }
                break;
            default:
                final String periodicityStr = periodicityEnum.toString();
                throw new InvalidUserInputException("Unable to understand periodicity value:" + periodicityStr);
        }
        return thePeriod;
    }

    /**
     * Returns a {@link SchedulingPeriod} (actually a {@link SpecificDay}) representing today.
     * 
     * @return The schedulingPeriod.
     */
    public static SpecificDay today() {
        return new SpecificDay(Calendar.getInstance().getTime());
    }
}
