"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const date_utils_1 = require("../../common/dates/date-utils");
const dates_1 = require("../../common/dates");
const DAY_UPDATE_KEYMAP = {
    ArrowRight: 1,
    ArrowLeft: -1,
    ArrowDown: 7,
    ArrowUp: -7,
};
class Calendar {
    onCreate(input) {
        this.locale = input.locale;
        const { firstDayOfWeek, weekdayLabels } = (0, date_utils_1.getWeekdayInfo)(input.locale);
        const todayISO = (0, date_utils_1.toLocalISO)(new Date());
        this.state = {
            focusISO: null,
            baseISO: todayISO,
            tabindexISO: todayISO,
            todayISO,
            offset: 0,
            firstDayOfWeek,
            weekdayLabels,
            rangeStart: null,
            rangeEnd: null,
            disableBefore: null,
            disableAfter: null,
            disableWeekdays: [],
            disableList: [],
        };
    }
    onInput(input) {
        var _a, _b, _c, _d, _e;
        if (input.locale !== this.locale) {
            this.locale = input.locale;
            const { firstDayOfWeek, weekdayLabels } = (0, date_utils_1.getWeekdayInfo)(input.locale);
            this.state.firstDayOfWeek = firstDayOfWeek;
            this.state.weekdayLabels = weekdayLabels;
        }
        if (input.todayISO) {
            const newTodayISO = (0, date_utils_1.toISO)(new Date(input.todayISO));
            this.state.todayISO = newTodayISO;
            this.state.baseISO = newTodayISO;
            this.state.tabindexISO = newTodayISO;
        }
        if (input.selected) {
            // If no selected times are visible, snap the view to the first one
            const selectedISOs = Array.isArray(input.selected)
                ? input.selected
                : [input.selected];
            const currFirstISO = this.getFirstVisibleISO();
            const currLastISO = this.getLastVisibleISO(input);
            const selectedTimeInView = selectedISOs.find((time) => time >= currFirstISO && time <= currLastISO);
            if (selectedTimeInView === undefined) {
                this.state.baseISO = this.state.tabindexISO = selectedISOs[0];
                this.state.offset = 0;
            }
        }
        this.calculateRangeDisplay(input);
        // handle changes regarding disabled dates
        this.state.disableBefore = (0, date_utils_1.dateArgToISO)(input.disableBefore);
        this.state.disableAfter = (0, date_utils_1.dateArgToISO)(input.disableAfter);
        this.state.disableWeekdays = (_a = input.disableWeekdays) !== null && _a !== void 0 ? _a : [];
        this.state.disableList = (_c = (_b = input.disableList) === null || _b === void 0 ? void 0 : _b.map(date_utils_1.dateArgToISO)) !== null && _c !== void 0 ? _c : [];
        if (this.isDisabled(this.state.tabindexISO)) {
            // The current tabindex is disabled, so we have to find a new one
            const firstActive = this.getFirstActiveISO(input);
            if (firstActive) {
                // This month has active days, so we can use the first one
                this.state.tabindexISO = firstActive;
            }
            else if (this.state.disableBefore &&
                this.state.tabindexISO < this.state.disableBefore) {
                // This month has no active days and the start of _possible_ dates is in the future, so we change months to the start of possible dates
                this.state.baseISO = this.state.disableBefore;
                this.state.offset = 0;
                this.state.tabindexISO =
                    (_d = this.getFirstActiveISO(input)) !== null && _d !== void 0 ? _d : this.state.disableBefore;
            }
            else if (this.state.disableAfter &&
                this.state.tabindexISO > this.state.disableAfter) {
                // This month has no active days and the end of _possible_ dates is in the past, so we change months to the end of possible dates
                this.state.baseISO = this.state.disableAfter;
                this.state.offset = 0;
                this.state.tabindexISO =
                    (_e = this.getFirstActiveISO(input)) !== null && _e !== void 0 ? _e : this.state.disableAfter;
            }
            else {
                // This may be reached in very specific edge cases, such as when the user has disabled all days in the current month manually
                // In this case, we leave the tabindex and position as is. This is a fall-through case.
            }
        }
        while (this.state.disableAfter &&
            this.getMonthDate(this.state.offset + (input.numMonths || 1) - 1).toISOString() > this.state.disableAfter) {
            this.state.offset--;
        }
    }
    isDisabled(iso) {
        return ((this.state.disableBefore && iso < this.state.disableBefore) ||
            (this.state.disableAfter && iso > this.state.disableAfter) ||
            this.state.disableWeekdays.includes((0, date_utils_1.fromISO)(iso).getUTCDay()) ||
            this.state.disableList.includes(iso));
    }
    onDaySelect(day) {
        this.emit("select", { iso: day });
    }
    onDayFocus(day) {
        this.state.focusISO = this.state.tabindexISO = day;
        this.calculateRangeDisplay();
    }
    onDayBlur() {
        this.state.focusISO = null;
        this.calculateRangeDisplay();
    }
    onKeyDown(event) {
        const dayChange = DAY_UPDATE_KEYMAP[event.key];
        if (dayChange) {
            event.preventDefault();
            // find new tabindex iso, skipping up to 7 disabled cells
            let tries = 7;
            let iso = this.state.tabindexISO;
            do {
                iso = (0, date_utils_1.offsetISO)(iso, dayChange);
            } while (tries-- > 0 && this.isDisabled(iso));
            if (tries > 0) {
                // check for edges of calendar
                const firstVisibleISO = this.getFirstVisibleISO();
                const lastVisibleISO = this.getLastVisibleISO();
                if (iso < firstVisibleISO) {
                    if (this.input.navigable)
                        this.prevMonth();
                    else
                        iso = firstVisibleISO;
                }
                else if (iso > lastVisibleISO) {
                    if (this.input.navigable)
                        this.nextMonth();
                    else
                        iso = lastVisibleISO;
                }
                this.setTabindexAndFocus(iso);
                this.emit("focus", { iso: this.state.tabindexISO });
            }
        }
        else {
            switch (event.key) {
                case "PageUp":
                    this.prevMonth(true);
                    break;
                case "PageDown":
                    this.nextMonth(true);
                    break;
                case "Home": {
                    const firstActiveISO = this.getFirstActiveISO();
                    if (firstActiveISO) {
                        this.setTabindexAndFocus(firstActiveISO);
                        this.emit("focus", { iso: this.state.tabindexISO });
                    }
                    break;
                }
                case "End": {
                    const lastActiveISO = this.getLastActiveISO();
                    if (lastActiveISO) {
                        this.setTabindexAndFocus(lastActiveISO);
                        this.emit("focus", { iso: this.state.tabindexISO });
                    }
                    break;
                }
                default:
            }
        }
    }
    getMonthDate(offset) {
        const baseDate = (0, date_utils_1.fromISO)(this.state.baseISO);
        const date = new Date(Date.UTC(baseDate.getUTCFullYear(), baseDate.getUTCMonth() + offset));
        return date;
    }
    getFirstVisibleISO() {
        return (0, date_utils_1.toISO)(this.getMonthDate(this.state.offset));
    }
    getLastVisibleISO(input = this.input) {
        const baseDate = (0, date_utils_1.fromISO)(this.state.baseISO);
        return (0, date_utils_1.toISO)(new Date(Date.UTC(baseDate.getUTCFullYear(), baseDate.getUTCMonth() +
            this.state.offset +
            (input.numMonths || 1), 0)));
    }
    getFirstActiveISO(input = this.input) {
        let iso = this.getFirstVisibleISO();
        const lastVisible = this.getLastVisibleISO(input);
        while (iso <= lastVisible && this.isDisabled(iso)) {
            iso = (0, date_utils_1.offsetISO)(iso, 1);
        }
        return iso > lastVisible ? null : iso;
    }
    getLastActiveISO(input = this.input) {
        let iso = this.getLastVisibleISO(input);
        const firstVisible = this.getFirstVisibleISO();
        while (iso >= firstVisible && this.isDisabled(iso)) {
            iso = (0, date_utils_1.offsetISO)(iso, -1);
        }
        return iso < firstVisible ? null : iso;
    }
    monthTitle(date) {
        const formatter = new Intl.DateTimeFormat((0, dates_1.localeDefault)(this.input.locale), {
            month: "long",
            year: "numeric",
        });
        return formatter.format(new Date(date.getUTCFullYear(), date.getUTCMonth()));
    }
    prevMonth(focus) {
        if (this.state.disableBefore &&
            this.getFirstVisibleISO() <= this.state.disableBefore) {
            return false;
        }
        this.state.offset--;
        let newTabindexISO = this.state.tabindexISO;
        const lastActiveISO = this.getLastActiveISO();
        if (lastActiveISO && this.state.tabindexISO > lastActiveISO) {
            newTabindexISO = this.state.tabindexISO = lastActiveISO;
        }
        if (focus) {
            this.setTabindexAndFocus(newTabindexISO);
        }
        this.emit("month-change", {
            iso: (0, date_utils_1.toISO)(this.getMonthDate(this.state.offset)),
        });
        return true;
    }
    nextMonth(focus) {
        if (this.state.disableAfter &&
            this.getLastVisibleISO() >= this.state.disableAfter) {
            return false;
        }
        this.state.offset++;
        let newTabindexISO = this.state.tabindexISO;
        const firstActiveISO = this.getFirstActiveISO();
        if (firstActiveISO && this.state.tabindexISO < firstActiveISO) {
            newTabindexISO = this.state.tabindexISO = firstActiveISO;
        }
        if (focus) {
            this.setTabindexAndFocus(newTabindexISO);
        }
        this.emit("month-change", {
            iso: (0, date_utils_1.toISO)(this.getMonthDate(this.state.offset + (this.input.numMonths || 1))),
        });
        return true;
    }
    setTabindexAndFocus(iso) {
        this.state.tabindexISO = iso;
        // After UI updates, focus on the new tabindex date
        setTimeout(() => { var _a; return (_a = this.getEl(iso)) === null || _a === void 0 ? void 0 : _a.focus(); });
    }
    calculateRangeDisplay(input = this.input) {
        if (input.selected && input.range) {
            // Determine range display (state.rangeStart-state.rangeEnd)
            let iso1, iso2;
            if (Array.isArray(input.selected)) {
                // Two elements are selected, we can use them as the ends of the range.
                [iso1, iso2] = input.selected;
            }
            else if (this.state.focusISO) {
                // One element is selected and the user is focused on a date,
                // so we use the selected component and the focus date instead
                iso1 = input.selected;
                iso2 = this.state.focusISO;
            }
            if (iso1 && iso2) {
                // Both ends of the range are valid, figure out which is first
                if (iso1 < iso2) {
                    this.state.rangeStart = iso1;
                    this.state.rangeEnd = iso2;
                }
                else {
                    this.state.rangeStart = iso2;
                    this.state.rangeEnd = iso1;
                }
            }
            else {
                // We can't display a range, so ensure that no range is highlighted
                this.state.rangeStart = this.state.rangeEnd = null;
            }
        }
        else {
            // We can't display a range, so ensure that no range is highlighted
            this.state.rangeStart = this.state.rangeEnd = null;
        }
    }
    isInRange(iso) {
        if (!this.state.rangeStart || !this.state.rangeEnd)
            // range doesn't exist
            return false;
        if (iso < this.state.rangeStart || iso > this.state.rangeEnd)
            // date is outside of range
            return false;
        return true;
    }
}
module.exports = Calendar;
