export class CronosDate {
    constructor(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) {
        this.year = year;
        this.month = month;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
        this.second = second;
    }
    static fromDate(date, timezone) {
        if (!timezone) {
            return new CronosDate(date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds());
        }
        return timezone['nativeDateToCronosDate'](date);
    }
    toDate(timezone) {
        if (!timezone) {
            return new Date(this.year, this.month - 1, this.day, this.hour, this.minute, this.second);
        }
        return timezone['cronosDateToNativeDate'](this);
    }
    static fromUTCTimestamp(timestamp) {
        const date = new Date(timestamp);
        return new CronosDate(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
    }
    toUTCTimestamp() {
        return Date.UTC(this.year, this.month - 1, this.day, this.hour, this.minute, this.second);
    }
    copyWith({ year = this.year, month = this.month, day = this.day, hour = this.hour, minute = this.minute, second = this.second } = {}) {
        return new CronosDate(year, month, day, hour, minute, second);
    }
}
// Adapted from Intl.DateTimeFormat timezone handling in https://github.com/moment/luxon
const ZoneCache = new Map();
export class CronosTimezone {
    constructor(IANANameOrOffset) {
        if (typeof IANANameOrOffset === 'number') {
            if (IANANameOrOffset > 840 || IANANameOrOffset < -840)
                throw new Error('Invalid offset');
            this.fixedOffset = IANANameOrOffset;
            return this;
        }
        const offsetMatch = IANANameOrOffset.match(/^([+-]?)(0[1-9]|1[0-4])(?::?([0-5][0-9]))?$/);
        if (offsetMatch) {
            this.fixedOffset = (offsetMatch[1] === '-' ? -1 : 1) * ((parseInt(offsetMatch[2], 10) * 60) + (parseInt(offsetMatch[3], 10) || 0));
            return this;
        }
        if (ZoneCache.has(IANANameOrOffset)) {
            return ZoneCache.get(IANANameOrOffset);
        }
        try {
            this.dateTimeFormat = new Intl.DateTimeFormat("en-US", {
                hour12: false,
                timeZone: IANANameOrOffset,
                year: "numeric",
                month: "2-digit",
                day: "2-digit",
                hour: "2-digit",
                minute: "2-digit",
                second: "2-digit"
            });
        }
        catch (err) {
            throw new Error('Invalid IANA name or offset');
        }
        this.zoneName = IANANameOrOffset;
        const currentYear = new Date().getUTCFullYear();
        this.winterOffset = this.offset(Date.UTC(currentYear, 0, 1));
        this.summerOffset = this.offset(Date.UTC(currentYear, 5, 1));
        ZoneCache.set(IANANameOrOffset, this);
    }
    toString() {
        if (this.fixedOffset) {
            const absOffset = Math.abs(this.fixedOffset);
            return [
                this.fixedOffset < 0 ? '-' : '+',
                Math.floor(absOffset / 60).toString().padStart(2, '0'),
                (absOffset % 60).toString().padStart(2, '0')
            ].join('');
        }
        return this.zoneName;
    }
    offset(ts) {
        if (!this.dateTimeFormat)
            return this.fixedOffset || 0;
        const date = new Date(ts);
        const { year, month, day, hour, minute, second } = this.nativeDateToCronosDate(date);
        const asUTC = Date.UTC(year, month - 1, day, hour, minute, second), asTS = ts - (ts % 1000);
        return (asUTC - asTS) / 60000;
    }
    nativeDateToCronosDate(date) {
        if (!this.dateTimeFormat) {
            return CronosDate['fromUTCTimestamp'](date.getTime() + (this.fixedOffset || 0) * 60000);
        }
        return this.dateTimeFormat['formatToParts']
            ? partsOffset(this.dateTimeFormat, date)
            : hackyOffset(this.dateTimeFormat, date);
    }
    cronosDateToNativeDate(date) {
        if (!this.dateTimeFormat) {
            return new Date(date['toUTCTimestamp']() - (this.fixedOffset || 0) * 60000);
        }
        const provisionalOffset = ((date.month > 3 || date.month < 11) ? this.summerOffset : this.winterOffset) || 0;
        const UTCTimestamp = date['toUTCTimestamp']();
        // Find the right offset a given local time.
        // Our UTC time is just a guess because our offset is just a guess
        let utcGuess = UTCTimestamp - provisionalOffset * 60000;
        // Test whether the zone matches the offset for this ts
        const o2 = this.offset(utcGuess);
        // If so, offset didn't change and we're done
        if (provisionalOffset === o2)
            return new Date(utcGuess);
        // If not, change the ts by the difference in the offset
        utcGuess -= (o2 - provisionalOffset) * 60000;
        // If that gives us the local time we want, we're done
        const o3 = this.offset(utcGuess);
        if (o2 === o3)
            return new Date(utcGuess);
        // If it's different, we're in a hole time. The offset has changed, but the we don't adjust the time
        return new Date(UTCTimestamp - Math.min(o2, o3) * 60000);
    }
}
function hackyOffset(dtf, date) {
    const formatted = dtf.format(date).replace(/\u200E/g, ""), parsed = formatted.match(/(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/), [, month, day, year, hour, minute, second] = (parsed !== null && parsed !== void 0 ? parsed : []).map(n => parseInt(n, 10));
    return new CronosDate(year, month, day, hour % 24, minute, second);
}
function partsOffset(dtf, date) {
    const formatted = dtf.formatToParts(date);
    return new CronosDate(parseInt(formatted[4].value, 10), parseInt(formatted[0].value, 10), parseInt(formatted[2].value, 10), parseInt(formatted[6].value, 10) % 24, parseInt(formatted[8].value, 10), parseInt(formatted[10].value, 10));
}
