"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CronDate = exports.DAYS_IN_MONTH = exports.DateMathOp = exports.TimeUnit = void 0;
const luxon_1 = require("luxon");
var TimeUnit;
(function (TimeUnit) {
    TimeUnit["Second"] = "Second";
    TimeUnit["Minute"] = "Minute";
    TimeUnit["Hour"] = "Hour";
    TimeUnit["Day"] = "Day";
    TimeUnit["Month"] = "Month";
    TimeUnit["Year"] = "Year";
})(TimeUnit || (exports.TimeUnit = TimeUnit = {}));
var DateMathOp;
(function (DateMathOp) {
    DateMathOp["Add"] = "Add";
    DateMathOp["Subtract"] = "Subtract";
})(DateMathOp || (exports.DateMathOp = DateMathOp = {}));
exports.DAYS_IN_MONTH = Object.freeze([31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]);
/**
 * CronDate class that wraps the Luxon DateTime object to provide
 * a consistent API for working with dates and times in the context of cron.
 */
class CronDate {
    #date;
    #dstStart = null;
    #dstEnd = null;
    /**
     * Maps the verb to the appropriate method
     */
    #verbMap = {
        add: {
            [TimeUnit.Year]: this.addYear.bind(this),
            [TimeUnit.Month]: this.addMonth.bind(this),
            [TimeUnit.Day]: this.addDay.bind(this),
            [TimeUnit.Hour]: this.addHour.bind(this),
            [TimeUnit.Minute]: this.addMinute.bind(this),
            [TimeUnit.Second]: this.addSecond.bind(this),
        },
        subtract: {
            [TimeUnit.Year]: this.subtractYear.bind(this),
            [TimeUnit.Month]: this.subtractMonth.bind(this),
            [TimeUnit.Day]: this.subtractDay.bind(this),
            [TimeUnit.Hour]: this.subtractHour.bind(this),
            [TimeUnit.Minute]: this.subtractMinute.bind(this),
            [TimeUnit.Second]: this.subtractSecond.bind(this),
        },
    };
    /**
     * Constructs a new CronDate instance.
     * @param {CronDate | Date | number | string} [timestamp] - The timestamp to initialize the CronDate with.
     * @param {string} [tz] - The timezone to use for the CronDate.
     */
    constructor(timestamp, tz) {
        const dateOpts = { zone: tz };
        // Initialize the internal DateTime object based on the type of timestamp provided.
        if (!timestamp) {
            this.#date = luxon_1.DateTime.local();
        }
        else if (timestamp instanceof CronDate) {
            this.#date = timestamp.#date;
            this.#dstStart = timestamp.#dstStart;
            this.#dstEnd = timestamp.#dstEnd;
        }
        else if (timestamp instanceof Date) {
            this.#date = luxon_1.DateTime.fromJSDate(timestamp, dateOpts);
        }
        else if (typeof timestamp === 'number') {
            this.#date = luxon_1.DateTime.fromMillis(timestamp, dateOpts);
        }
        else {
            this.#date = luxon_1.DateTime.fromISO(timestamp, dateOpts);
            this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(timestamp, dateOpts));
            this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(timestamp, dateOpts));
            this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(timestamp, 'EEE, d MMM yyyy HH:mm:ss', dateOpts));
        }
        // Check for valid DateTime and throw an error if not valid.
        if (!this.#date.isValid) {
            throw new Error(`CronDate: unhandled timestamp: ${timestamp}`);
        }
        // Set the timezone if it is provided and different from the current zone.
        if (tz && tz !== this.#date.zoneName) {
            this.#date = this.#date.setZone(tz);
        }
    }
    /**
     * Determines if the given year is a leap year.
     * @param {number} year - The year to check
     * @returns {boolean} - True if the year is a leap year, false otherwise
     * @private
     */
    static #isLeapYear(year) {
        return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
    }
    /**
     * Returns daylight savings start time.
     * @returns {number | null}
     */
    get dstStart() {
        return this.#dstStart;
    }
    /**
     * Sets daylight savings start time.
     * @param {number | null} value
     */
    set dstStart(value) {
        this.#dstStart = value;
    }
    /**
     * Returns daylight savings end time.
     * @returns {number | null}
     */
    get dstEnd() {
        return this.#dstEnd;
    }
    /**
     * Sets daylight savings end time.
     * @param {number | null} value
     */
    set dstEnd(value) {
        this.#dstEnd = value;
    }
    /**
     * Adds one year to the current CronDate.
     */
    addYear() {
        this.#date = this.#date.plus({ years: 1 });
    }
    /**
     * Adds one month to the current CronDate.
     */
    addMonth() {
        this.#date = this.#date.plus({ months: 1 }).startOf('month');
    }
    /**
     * Adds one day to the current CronDate.
     */
    addDay() {
        this.#date = this.#date.plus({ days: 1 }).startOf('day');
    }
    /**
     * Adds one hour to the current CronDate.
     */
    addHour() {
        this.#date = this.#date.plus({ hours: 1 }).startOf('hour');
    }
    /**
     * Adds one minute to the current CronDate.
     */
    addMinute() {
        this.#date = this.#date.plus({ minutes: 1 }).startOf('minute');
    }
    /**
     * Adds one second to the current CronDate.
     */
    addSecond() {
        this.#date = this.#date.plus({ seconds: 1 });
    }
    /**
     * Subtracts one year from the current CronDate.
     */
    subtractYear() {
        this.#date = this.#date.minus({ years: 1 });
    }
    /**
     * Subtracts one month from the current CronDate.
     * If the month is 1, it will subtract one year instead.
     */
    subtractMonth() {
        this.#date = this.#date.minus({ months: 1 }).endOf('month').startOf('second');
    }
    /**
     * Subtracts one day from the current CronDate.
     * If the day is 1, it will subtract one month instead.
     */
    subtractDay() {
        this.#date = this.#date.minus({ days: 1 }).endOf('day').startOf('second');
    }
    /**
     * Subtracts one hour from the current CronDate.
     * If the hour is 0, it will subtract one day instead.
     */
    subtractHour() {
        this.#date = this.#date.minus({ hours: 1 }).endOf('hour').startOf('second');
    }
    /**
     * Subtracts one minute from the current CronDate.
     * If the minute is 0, it will subtract one hour instead.
     */
    subtractMinute() {
        this.#date = this.#date.minus({ minutes: 1 }).endOf('minute').startOf('second');
    }
    /**
     * Subtracts one second from the current CronDate.
     * If the second is 0, it will subtract one minute instead.
     */
    subtractSecond() {
        this.#date = this.#date.minus({ seconds: 1 });
    }
    /**
     * Adds a unit of time to the current CronDate.
     * @param {TimeUnit} unit
     */
    addUnit(unit) {
        this.#verbMap.add[unit]();
    }
    /**
     * Subtracts a unit of time from the current CronDate.
     * @param {TimeUnit} unit
     */
    subtractUnit(unit) {
        this.#verbMap.subtract[unit]();
    }
    /**
     * Handles a math operation.
     * @param {DateMathOp} verb - {'add' | 'subtract'}
     * @param {TimeUnit} unit - {'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'}
     */
    invokeDateOperation(verb, unit) {
        if (verb === DateMathOp.Add) {
            this.addUnit(unit);
            return;
        }
        if (verb === DateMathOp.Subtract) {
            this.subtractUnit(unit);
            return;
        }
        /* istanbul ignore next - this would only happen if an end user call the handleMathOp with an invalid verb */
        throw new Error(`Invalid verb: ${verb}`);
    }
    /**
     * Returns the day.
     * @returns {number}
     */
    getDate() {
        return this.#date.day;
    }
    /**
     * Returns the year.
     * @returns {number}
     */
    getFullYear() {
        return this.#date.year;
    }
    /**
     * Returns the day of the week.
     * @returns {number}
     */
    getDay() {
        const weekday = this.#date.weekday;
        return weekday === 7 ? 0 : weekday;
    }
    /**
     * Returns the month.
     * @returns {number}
     */
    getMonth() {
        return this.#date.month - 1;
    }
    /**
     * Returns the hour.
     * @returns {number}
     */
    getHours() {
        return this.#date.hour;
    }
    /**
     * Returns the minutes.
     * @returns {number}
     */
    getMinutes() {
        return this.#date.minute;
    }
    /**
     * Returns the seconds.
     * @returns {number}
     */
    getSeconds() {
        return this.#date.second;
    }
    /**
     * Returns the milliseconds.
     * @returns {number}
     */
    getMilliseconds() {
        return this.#date.millisecond;
    }
    /**
     * Returns the time.
     * @returns {number}
     */
    getTime() {
        return this.#date.valueOf();
    }
    /**
     * Returns the UTC day.
     * @returns {number}
     */
    getUTCDate() {
        return this.#getUTC().day;
    }
    /**
     * Returns the UTC year.
     * @returns {number}
     */
    getUTCFullYear() {
        return this.#getUTC().year;
    }
    /**
     * Returns the UTC day of the week.
     * @returns {number}
     */
    getUTCDay() {
        const weekday = this.#getUTC().weekday;
        return weekday === 7 ? 0 : weekday;
    }
    /**
     * Returns the UTC month.
     * @returns {number}
     */
    getUTCMonth() {
        return this.#getUTC().month - 1;
    }
    /**
     * Returns the UTC hour.
     * @returns {number}
     */
    getUTCHours() {
        return this.#getUTC().hour;
    }
    /**
     * Returns the UTC minutes.
     * @returns {number}
     */
    getUTCMinutes() {
        return this.#getUTC().minute;
    }
    /**
     * Returns the UTC seconds.
     * @returns {number}
     */
    getUTCSeconds() {
        return this.#getUTC().second;
    }
    /**
     * Returns the UTC milliseconds.
     * @returns {string | null}
     */
    toISOString() {
        return this.#date.toUTC().toISO();
    }
    /**
     * Returns the date as a JSON string.
     * @returns {string | null}
     */
    toJSON() {
        return this.#date.toJSON();
    }
    /**
     * Sets the day.
     * @param d
     */
    setDate(d) {
        this.#date = this.#date.set({ day: d });
    }
    /**
     * Sets the year.
     * @param y
     */
    setFullYear(y) {
        this.#date = this.#date.set({ year: y });
    }
    /**
     * Sets the day of the week.
     * @param d
     */
    setDay(d) {
        this.#date = this.#date.set({ weekday: d });
    }
    /**
     * Sets the month.
     * @param m
     */
    setMonth(m) {
        this.#date = this.#date.set({ month: m + 1 });
    }
    /**
     * Sets the hour.
     * @param h
     */
    setHours(h) {
        this.#date = this.#date.set({ hour: h });
    }
    /**
     * Sets the minutes.
     * @param m
     */
    setMinutes(m) {
        this.#date = this.#date.set({ minute: m });
    }
    /**
     * Sets the seconds.
     * @param s
     */
    setSeconds(s) {
        this.#date = this.#date.set({ second: s });
    }
    /**
     * Sets the milliseconds.
     * @param s
     */
    setMilliseconds(s) {
        this.#date = this.#date.set({ millisecond: s });
    }
    /**
     * Returns the date as a string.
     * @returns {string}
     */
    toString() {
        return this.toDate().toString();
    }
    /**
     * Returns the date as a Date object.
     * @returns {Date}
     */
    toDate() {
        return this.#date.toJSDate();
    }
    /**
     * Returns true if the day is the last day of the month.
     * @returns {boolean}
     */
    isLastDayOfMonth() {
        const { day, month } = this.#date;
        // Special handling for February in leap years
        if (month === 2) {
            const isLeap = CronDate.#isLeapYear(this.#date.year);
            return day === exports.DAYS_IN_MONTH[month - 1] - (isLeap ? 0 : 1);
        }
        // For other months, check against the static map
        return day === exports.DAYS_IN_MONTH[month - 1];
    }
    /**
     * Returns true if the day is the last weekday of the month.
     * @returns {boolean}
     */
    isLastWeekdayOfMonth() {
        const { day, month } = this.#date;
        // Get the last day of the current month
        let lastDay;
        if (month === 2) {
            // Special handling for February
            lastDay = exports.DAYS_IN_MONTH[month - 1] - (CronDate.#isLeapYear(this.#date.year) ? 0 : 1);
        }
        else {
            lastDay = exports.DAYS_IN_MONTH[month - 1];
        }
        // Check if the current day is within 7 days of the end of the month
        return day > lastDay - 7;
    }
    /**
     * Primarily for internal use.
     * @param {DateMathOp} op - The operation to perform.
     * @param {TimeUnit} unit - The unit of time to use.
     * @param {number} [hoursLength] - The length of the hours. Required when unit is not month or day.
     */
    applyDateOperation(op, unit, hoursLength) {
        if (unit === TimeUnit.Month || unit === TimeUnit.Day) {
            this.invokeDateOperation(op, unit);
            return;
        }
        const previousHour = this.getHours();
        this.invokeDateOperation(op, unit);
        const currentHour = this.getHours();
        const diff = currentHour - previousHour;
        if (diff === 2) {
            if (hoursLength !== 24) {
                this.dstStart = currentHour;
            }
        }
        else if (diff === 0 && this.getMinutes() === 0 && this.getSeconds() === 0) {
            if (hoursLength !== 24) {
                this.dstEnd = currentHour;
            }
        }
    }
    /**
     * Returns the UTC date.
     * @private
     * @returns {DateTime}
     */
    #getUTC() {
        return this.#date.toUTC();
    }
}
exports.CronDate = CronDate;
exports.default = CronDate;
