/*!
 * (C) Ionic http://ionicframework.com - MIT License
 */
import { isAfter, isBefore, isSameDay } from "./comparison";
import { getLocalizedDayPeriod, removeDateTzOffset, getFormattedHour, addTimePadding, getTodayLabel, getYear, } from "./format";
import { getNumDaysInMonth, is24Hour, getHourCycle } from "./helpers";
import { getNextMonth, getPreviousMonth, getInternalHourValue } from "./manipulation";
/**
 * Returns the current date as
 * an ISO string in the user's
 * time zone.
 */
export const getToday = () => {
    /**
     * ion-datetime intentionally does not
     * parse time zones/do automatic time zone
     * conversion when accepting user input.
     * However when we get today's date string,
     * we want it formatted relative to the user's
     * time zone.
     *
     * When calling toISOString(), the browser
     * will convert the date to UTC time by either adding
     * or subtracting the time zone offset.
     * To work around this, we need to either add
     * or subtract the time zone offset to the Date
     * object prior to calling toISOString().
     * This allows us to get an ISO string
     * that is in the user's time zone.
     */
    return removeDateTzOffset(new Date()).toISOString();
};
const minutes = [
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
];
// h11 hour system uses 0-11. Midnight starts at 0:00am.
const hour11 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
// h12 hour system uses 0-12. Midnight starts at 12:00am.
const hour12 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
// h23 hour system uses 0-23. Midnight starts at 0:00.
const hour23 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];
// h24 hour system uses 1-24. Midnight starts at 24:00.
const hour24 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 0];
/**
 * Given a locale and a theme,
 * return an array with formatted days
 * of the week. iOS and Ionic themes should
 * display days such as "Mon" or "Tue".
 * MD should display days such as "M"
 * or "T".
 */
export const getDaysOfWeek = (locale, theme, firstDayOfWeek = 0) => {
    /**
     * Nov 1st, 2020 starts on a Sunday.
     * ion-datetime assumes weeks start on Sunday,
     * but is configurable via `firstDayOfWeek`.
     */
    const weekdayFormat = theme === 'md' ? 'narrow' : 'short';
    const intl = new Intl.DateTimeFormat(locale, { weekday: weekdayFormat });
    const startDate = new Date('11/01/2020');
    const daysOfWeek = [];
    /**
     * For each day of the week,
     * get the day name.
     */
    for (let i = firstDayOfWeek; i < firstDayOfWeek + 7; i++) {
        const currentDate = new Date(startDate);
        currentDate.setDate(currentDate.getDate() + i);
        daysOfWeek.push(intl.format(currentDate));
    }
    return daysOfWeek;
};
/**
 * Returns an array containing all of the
 * days in a month for a given year. Values are
 * aligned with a week calendar starting on
 * the firstDayOfWeek value (Sunday by default)
 * using null values.
 */
export const getDaysOfMonth = (month, year, firstDayOfWeek, showAdjacentDays = false) => {
    const numDays = getNumDaysInMonth(month, year);
    let previousNumDays; //previous month number of days
    if (month === 1) {
        // If the current month is January, the previous month should be December of the previous year.
        previousNumDays = getNumDaysInMonth(12, year - 1);
    }
    else {
        // Otherwise, the previous month should be the current month - 1 of the same year.
        previousNumDays = getNumDaysInMonth(month - 1, year);
    }
    const firstOfMonth = new Date(`${month}/1/${year}`).getDay();
    /**
     * To get the first day of the month aligned on the correct
     * day of the week, we need to determine how many "filler" days
     * to generate. These filler days as empty/disabled buttons
     * that fill the space of the days of the week before the first
     * of the month.
     *
     * There are two cases here:
     *
     * 1. If firstOfMonth = 4, firstDayOfWeek = 0 then the offset
     * is (4 - (0 + 1)) = 3. Since the offset loop goes from 0 to 3 inclusive,
     * this will generate 4 filler days (0, 1, 2, 3), and then day of week 4 will have
     * the first day of the month.
     *
     * 2. If firstOfMonth = 2, firstDayOfWeek = 4 then the offset
     * is (6 - (4 - 2)) = 4. Since the offset loop goes from 0 to 4 inclusive,
     * this will generate 5 filler days (0, 1, 2, 3, 4), and then day of week 5 will have
     * the first day of the month.
     */
    const offset = firstOfMonth >= firstDayOfWeek ? firstOfMonth - (firstDayOfWeek + 1) : 6 - (firstDayOfWeek - firstOfMonth);
    let days = [];
    for (let i = 1; i <= numDays; i++) {
        days.push({ day: i, dayOfWeek: (offset + i) % 7, isAdjacentDay: false });
    }
    if (showAdjacentDays) {
        for (let i = 0; i <= offset; i++) {
            // Using offset create previous month adjacent day, starting from last day
            days = [{ day: previousNumDays - i, dayOfWeek: (previousNumDays - i) % 7, isAdjacentDay: true }, ...days];
        }
        // Calculate positiveOffset
        // The calendar will display 42 days (6 rows of 7 columns)
        // Knowing this the offset is 41 (we start at index 0)
        // minus (the previous offset + the current month days)
        const positiveOffset = 41 - (numDays + offset);
        for (let i = 0; i < positiveOffset; i++) {
            days.push({ day: i + 1, dayOfWeek: (numDays + offset + i) % 7, isAdjacentDay: true });
        }
    }
    else {
        for (let i = 0; i <= offset; i++) {
            days = [{ day: null, dayOfWeek: null, isAdjacentDay: false }, ...days];
        }
    }
    return days;
};
/**
 * Returns an array of pre-defined hour
 * values based on the provided hourCycle.
 */
const getHourData = (hourCycle) => {
    switch (hourCycle) {
        case 'h11':
            return hour11;
        case 'h12':
            return hour12;
        case 'h23':
            return hour23;
        case 'h24':
            return hour24;
        default:
            throw new Error(`Invalid hour cycle "${hourCycle}"`);
    }
};
/**
 * Given a local, reference datetime parts and option
 * max/min bound datetime parts, calculate the acceptable
 * hour and minute values according to the bounds and locale.
 */
export const generateTime = (locale, refParts, hourCycle = 'h12', minParts, maxParts, hourValues, minuteValues) => {
    const computedHourCycle = getHourCycle(locale, hourCycle);
    const use24Hour = is24Hour(computedHourCycle);
    let processedHours = getHourData(computedHourCycle);
    let processedMinutes = minutes;
    let isAMAllowed = true;
    let isPMAllowed = true;
    if (hourValues) {
        processedHours = processedHours.filter((hour) => hourValues.includes(hour));
    }
    if (minuteValues) {
        processedMinutes = processedMinutes.filter((minute) => minuteValues.includes(minute));
    }
    if (minParts) {
        /**
         * If ref day is the same as the
         * minimum allowed day, filter hour/minute
         * values according to min hour and minute.
         */
        if (isSameDay(refParts, minParts)) {
            /**
             * Users may not always set the hour/minute for
             * min value (i.e. 2021-06-02) so we should allow
             * all hours/minutes in that case.
             */
            if (minParts.hour !== undefined) {
                processedHours = processedHours.filter((hour) => {
                    const convertedHour = refParts.ampm === 'pm' ? (hour + 12) % 24 : hour;
                    return (use24Hour ? hour : convertedHour) >= minParts.hour;
                });
                isAMAllowed = minParts.hour < 13;
            }
            if (minParts.minute !== undefined) {
                /**
                 * The minimum minute range should not be enforced when
                 * the hour is greater than the min hour.
                 *
                 * For example with a minimum range of 09:30, users
                 * should be able to select 10:00-10:29 and beyond.
                 */
                let isPastMinHour = false;
                if (minParts.hour !== undefined && refParts.hour !== undefined) {
                    if (refParts.hour > minParts.hour) {
                        isPastMinHour = true;
                    }
                }
                processedMinutes = processedMinutes.filter((minute) => {
                    if (isPastMinHour) {
                        return true;
                    }
                    return minute >= minParts.minute;
                });
            }
            /**
             * If ref day is before minimum
             * day do not render any hours/minute values
             */
        }
        else if (isBefore(refParts, minParts)) {
            processedHours = [];
            processedMinutes = [];
            isAMAllowed = isPMAllowed = false;
        }
    }
    if (maxParts) {
        /**
         * If ref day is the same as the
         * maximum allowed day, filter hour/minute
         * values according to max hour and minute.
         */
        if (isSameDay(refParts, maxParts)) {
            /**
             * Users may not always set the hour/minute for
             * max value (i.e. 2021-06-02) so we should allow
             * all hours/minutes in that case.
             */
            if (maxParts.hour !== undefined) {
                processedHours = processedHours.filter((hour) => {
                    const convertedHour = refParts.ampm === 'pm' ? (hour + 12) % 24 : hour;
                    return (use24Hour ? hour : convertedHour) <= maxParts.hour;
                });
                isPMAllowed = maxParts.hour >= 12;
            }
            if (maxParts.minute !== undefined && refParts.hour === maxParts.hour) {
                // The available minutes should only be filtered when the hour is the same as the max hour.
                // For example if the max hour is 10:30 and the current hour is 10:00,
                // users should be able to select 00-30 minutes.
                // If the current hour is 09:00, users should be able to select 00-60 minutes.
                processedMinutes = processedMinutes.filter((minute) => minute <= maxParts.minute);
            }
            /**
             * If ref day is after minimum
             * day do not render any hours/minute values
             */
        }
        else if (isAfter(refParts, maxParts)) {
            processedHours = [];
            processedMinutes = [];
            isAMAllowed = isPMAllowed = false;
        }
    }
    return {
        hours: processedHours,
        minutes: processedMinutes,
        am: isAMAllowed,
        pm: isPMAllowed,
    };
};
/**
 * Given DatetimeParts, generate the previous,
 * current, and and next months.
 */
export const generateMonths = (refParts, forcedDate) => {
    const current = { month: refParts.month, year: refParts.year, day: refParts.day };
    /**
     * If we're forcing a month to appear, and it's different from the current month,
     * ensure it appears by replacing the next or previous month as appropriate.
     */
    if (forcedDate !== undefined && (refParts.month !== forcedDate.month || refParts.year !== forcedDate.year)) {
        const forced = { month: forcedDate.month, year: forcedDate.year, day: forcedDate.day };
        const forcedMonthIsBefore = isBefore(forced, current);
        return forcedMonthIsBefore
            ? [forced, current, getNextMonth(refParts)]
            : [getPreviousMonth(refParts), current, forced];
    }
    return [getPreviousMonth(refParts), current, getNextMonth(refParts)];
};
export const getMonthColumnData = (locale, refParts, minParts, maxParts, monthValues, formatOptions = {
    month: 'long',
}) => {
    const { year } = refParts;
    const months = [];
    if (monthValues !== undefined) {
        let processedMonths = monthValues;
        if ((maxParts === null || maxParts === void 0 ? void 0 : maxParts.month) !== undefined) {
            processedMonths = processedMonths.filter((month) => month <= maxParts.month);
        }
        if ((minParts === null || minParts === void 0 ? void 0 : minParts.month) !== undefined) {
            processedMonths = processedMonths.filter((month) => month >= minParts.month);
        }
        processedMonths.forEach((processedMonth) => {
            const date = new Date(`${processedMonth}/1/${year} GMT+0000`);
            const monthString = new Intl.DateTimeFormat(locale, Object.assign(Object.assign({}, formatOptions), { timeZone: 'UTC' })).format(date);
            months.push({ text: monthString, value: processedMonth });
        });
    }
    else {
        const maxMonth = maxParts && maxParts.year === year ? maxParts.month : 12;
        const minMonth = minParts && minParts.year === year ? minParts.month : 1;
        for (let i = minMonth; i <= maxMonth; i++) {
            /**
             *
             * There is a bug on iOS 14 where
             * Intl.DateTimeFormat takes into account
             * the local timezone offset when formatting dates.
             *
             * Forcing the timezone to 'UTC' fixes the issue. However,
             * we should keep this workaround as it is safer. In the event
             * this breaks in another browser, we will not be impacted
             * because all dates will be interpreted in UTC.
             *
             * Example:
             * new Intl.DateTimeFormat('en-US', { month: 'long' }).format(new Date('Sat Apr 01 2006 00:00:00 GMT-0400 (EDT)')) // "March"
             * new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' }).format(new Date('Sat Apr 01 2006 00:00:00 GMT-0400 (EDT)')) // "April"
             *
             * In certain timezones, iOS 14 shows the wrong
             * date for .toUTCString(). To combat this, we
             * force all of the timezones to GMT+0000 (UTC).
             *
             * Example:
             * Time Zone: Central European Standard Time
             * new Date('1/1/1992').toUTCString() // "Tue, 31 Dec 1991 23:00:00 GMT"
             * new Date('1/1/1992 GMT+0000').toUTCString() // "Wed, 01 Jan 1992 00:00:00 GMT"
             */
            const date = new Date(`${i}/1/${year} GMT+0000`);
            const monthString = new Intl.DateTimeFormat(locale, Object.assign(Object.assign({}, formatOptions), { timeZone: 'UTC' })).format(date);
            months.push({ text: monthString, value: i });
        }
    }
    return months;
};
/**
 * Returns information regarding
 * selectable dates (i.e 1st, 2nd, 3rd, etc)
 * within a reference month.
 * @param locale The locale to format the date with
 * @param refParts The reference month/year to generate dates for
 * @param minParts The minimum bound on the date that can be returned
 * @param maxParts The maximum bound on the date that can be returned
 * @param dayValues The allowed date values
 * @returns Date data to be used in ion-picker-column
 */
export const getDayColumnData = (locale, refParts, minParts, maxParts, dayValues, formatOptions = {
    day: 'numeric',
}) => {
    const { month, year } = refParts;
    const days = [];
    /**
     * If we have max/min bounds that in the same
     * month/year as the refParts, we should
     * use the define day as the max/min day.
     * Otherwise, fallback to the max/min days in a month.
     */
    const numDaysInMonth = getNumDaysInMonth(month, year);
    const maxDay = (maxParts === null || maxParts === void 0 ? void 0 : maxParts.day) !== null && (maxParts === null || maxParts === void 0 ? void 0 : maxParts.day) !== undefined && maxParts.year === year && maxParts.month === month
        ? maxParts.day
        : numDaysInMonth;
    const minDay = (minParts === null || minParts === void 0 ? void 0 : minParts.day) !== null && (minParts === null || minParts === void 0 ? void 0 : minParts.day) !== undefined && minParts.year === year && minParts.month === month
        ? minParts.day
        : 1;
    if (dayValues !== undefined) {
        let processedDays = dayValues;
        processedDays = processedDays.filter((day) => day >= minDay && day <= maxDay);
        processedDays.forEach((processedDay) => {
            const date = new Date(`${month}/${processedDay}/${year} GMT+0000`);
            const dayString = new Intl.DateTimeFormat(locale, Object.assign(Object.assign({}, formatOptions), { timeZone: 'UTC' })).format(date);
            days.push({ text: dayString, value: processedDay });
        });
    }
    else {
        for (let i = minDay; i <= maxDay; i++) {
            const date = new Date(`${month}/${i}/${year} GMT+0000`);
            const dayString = new Intl.DateTimeFormat(locale, Object.assign(Object.assign({}, formatOptions), { timeZone: 'UTC' })).format(date);
            days.push({ text: dayString, value: i });
        }
    }
    return days;
};
export const getYearColumnData = (locale, refParts, minParts, maxParts, yearValues) => {
    var _a, _b;
    let processedYears = [];
    if (yearValues !== undefined) {
        processedYears = yearValues;
        if ((maxParts === null || maxParts === void 0 ? void 0 : maxParts.year) !== undefined) {
            processedYears = processedYears.filter((year) => year <= maxParts.year);
        }
        if ((minParts === null || minParts === void 0 ? void 0 : minParts.year) !== undefined) {
            processedYears = processedYears.filter((year) => year >= minParts.year);
        }
    }
    else {
        const { year } = refParts;
        const maxYear = (_a = maxParts === null || maxParts === void 0 ? void 0 : maxParts.year) !== null && _a !== void 0 ? _a : year;
        const minYear = (_b = minParts === null || minParts === void 0 ? void 0 : minParts.year) !== null && _b !== void 0 ? _b : year - 100;
        for (let i = minYear; i <= maxYear; i++) {
            processedYears.push(i);
        }
    }
    return processedYears.map((year) => ({
        text: getYear(locale, { year, month: refParts.month, day: refParts.day }),
        value: year,
    }));
};
/**
 * Given a starting date and an upper bound,
 * this functions returns an array of all
 * month objects in that range.
 */
const getAllMonthsInRange = (currentParts, maxParts) => {
    if (currentParts.month === maxParts.month && currentParts.year === maxParts.year) {
        return [currentParts];
    }
    return [currentParts, ...getAllMonthsInRange(getNextMonth(currentParts), maxParts)];
};
/**
 * Creates and returns picker items
 * that represent the days in a month.
 * Example: "Thu, Jun 2"
 */
export const getCombinedDateColumnData = (locale, todayParts, minParts, maxParts, dayValues, monthValues) => {
    let items = [];
    let parts = [];
    /**
     * Get all month objects from the min date
     * to the max date. Note: Do not use getMonthColumnData
     * as that function only generates dates within a
     * single year.
     */
    let months = getAllMonthsInRange(minParts, maxParts);
    /**
     * Filter out any disallowed month values.
     */
    if (monthValues) {
        months = months.filter(({ month }) => monthValues.includes(month));
    }
    /**
     * Get all of the days in the month.
     * From there, generate an array where
     * each item has the month, date, and day
     * of work as the text.
     */
    months.forEach((monthObject) => {
        const referenceMonth = { month: monthObject.month, day: null, year: monthObject.year };
        const monthDays = getDayColumnData(locale, referenceMonth, minParts, maxParts, dayValues, {
            month: 'short',
            day: 'numeric',
            weekday: 'short',
        });
        const dateParts = [];
        const dateColumnItems = [];
        monthDays.forEach((dayObject) => {
            const isToday = isSameDay(Object.assign(Object.assign({}, referenceMonth), { day: dayObject.value }), todayParts);
            /**
             * Today's date should read as "Today" (localized)
             * not the actual date string
             */
            dateColumnItems.push({
                text: isToday ? getTodayLabel(locale) : dayObject.text,
                value: `${referenceMonth.year}-${referenceMonth.month}-${dayObject.value}`,
            });
            /**
             * When selecting a date in the wheel picker
             * we need access to the raw datetime parts data.
             * The picker column only accepts values of
             * type string or number, so we need to return
             * two sets of data: A data set to be passed
             * to the picker column, and a data set to
             * be used to reference the raw data when
             * updating the picker column value.
             */
            dateParts.push({
                month: referenceMonth.month,
                year: referenceMonth.year,
                day: dayObject.value,
            });
        });
        parts = [...parts, ...dateParts];
        items = [...items, ...dateColumnItems];
    });
    return {
        parts,
        items,
    };
};
export const getTimeColumnsData = (locale, refParts, hourCycle, minParts, maxParts, allowedHourValues, allowedMinuteValues) => {
    const computedHourCycle = getHourCycle(locale, hourCycle);
    const use24Hour = is24Hour(computedHourCycle);
    const { hours, minutes, am, pm } = generateTime(locale, refParts, computedHourCycle, minParts, maxParts, allowedHourValues, allowedMinuteValues);
    const hoursItems = hours.map((hour) => {
        return {
            text: getFormattedHour(hour, computedHourCycle),
            value: getInternalHourValue(hour, use24Hour, refParts.ampm),
        };
    });
    const minutesItems = minutes.map((minute) => {
        return {
            text: addTimePadding(minute),
            value: minute,
        };
    });
    const dayPeriodItems = [];
    if (am && !use24Hour) {
        dayPeriodItems.push({
            text: getLocalizedDayPeriod(locale, 'am'),
            value: 'am',
        });
    }
    if (pm && !use24Hour) {
        dayPeriodItems.push({
            text: getLocalizedDayPeriod(locale, 'pm'),
            value: 'pm',
        });
    }
    return {
        minutesData: minutesItems,
        hoursData: hoursItems,
        dayPeriodData: dayPeriodItems,
    };
};
