"use strict";
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _ExifDateTime_dt;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExifDateTime = void 0;
const luxon_1 = require("luxon");
const DateTime_1 = require("./DateTime");
const Maybe_1 = require("./Maybe");
const Object_1 = require("./Object");
const String_1 = require("./String");
const Timezones_1 = require("./Timezones");
const TimeFmts = [
    { fmt: "HH:mm:ss.u", unsetMilliseconds: false },
    { fmt: "HH:mm:ss", unsetMilliseconds: true },
    { fmt: "HH:mm", unsetMilliseconds: true },
];
/**
 * Encodes an ExifDateTime with an optional tz offset in minutes.
 */
class ExifDateTime {
    static fromISO(iso, defaultZone) {
        if ((0, String_1.blank)(iso) || null != iso.match(/^\d+$/))
            return undefined;
        // Unfortunately, DateTime.fromISO() is happy to parse a date with no time,
        // so we have to do this ourselves:
        const patterns = [];
        for (const z of [
            { fmt: "ZZ", zone: undefined },
            { fmt: "'Z'", zone: "UTC" },
            { fmt: "", zone: defaultZone },
        ]) {
            for (const sep of ["'T'", " "]) {
                for (const timeFmt of TimeFmts) {
                    patterns.push({
                        fmt: `y-M-d${sep}${timeFmt.fmt}${z.fmt}`,
                        zone: z.zone,
                        unsetMilliseconds: timeFmt.unsetMilliseconds,
                    });
                }
            }
        }
        return this.fromPatterns(iso, patterns);
    }
    /**
     * Try to parse a date-time string from EXIF. If there is not both a date
     * and a time component, returns `undefined`.
     *
     * @param text from EXIF metadata
     * @param defaultZone a "zone name" to use as a backstop, or default, if
     * `text` doesn't specify a zone. This may be IANA-formatted, like
     * "America/Los_Angeles", or an offset, like "UTC-3". See
     * `offsetMinutesToZoneName`.
     */
    static fromEXIF(text, defaultZone) {
        var _a;
        if ((0, String_1.blank)(text))
            return undefined;
        return (
        // .fromExifStrict() uses .fromISO() as a backstop
        (_a = this.fromExifStrict(text, defaultZone)) !== null && _a !== void 0 ? _a : this.fromExifLoose(text, defaultZone));
    }
    static fromPatterns(text, fmts) {
        const s = (0, String_1.toS)(text).trim();
        const inputs = [s];
        // Some EXIF datetime will "over-specify" and include both the utc offset
        // *and* the "time zone abbreviation", like PST or PDT.
        // TZAs are between 2 (AT) and 5 (WEST) characters.
        // Unfortunately, luxon doesn't support regex.
        // We only want to strip off the TZA if it isn't "UTC" or "Z"
        if (null == s.match(/[.\d\s](UTC|Z)$/)) {
            const noTza = s.replace(/ [a-z]{2,5}$/i, "");
            if (noTza !== s)
                inputs.push(noTza);
        }
        // PERF: unroll first() to avoid creating closures
        for (const input of inputs) {
            for (const { fmt, zone, unsetMilliseconds } of fmts) {
                const dt = luxon_1.DateTime.fromFormat(input, fmt, {
                    setZone: true,
                    zone: zone !== null && zone !== void 0 ? zone : Timezones_1.UnsetZone,
                });
                const edt = ExifDateTime.fromDateTime(dt, {
                    rawValue: s,
                    unsetMilliseconds: unsetMilliseconds !== null && unsetMilliseconds !== void 0 ? unsetMilliseconds : false,
                });
                if (edt != null)
                    return edt;
            }
        }
        return;
    }
    /**
     * Parse the given date-time string, EXIF-formatted.
     *
     * @param text from EXIF metadata, in `y:M:d H:m:s` format (with optional
     * sub-seconds and/or timezone)
  
     * @param defaultZone a "zone name" to use as a backstop, or default, if
     * `text` doesn't specify a zone. This may be IANA-formatted, like
     * "America/Los_Angeles", or an offset, like "UTC-3". See
     * `offsetMinutesToZoneName`.
     */
    static fromExifStrict(text, defaultZone) {
        var _a;
        if ((0, String_1.blank)(text))
            return undefined;
        const patterns = [];
        for (const z of [
            { fmt: "ZZ", zone: undefined },
            { fmt: "'Z'", zone: "UTC" },
            { fmt: "", zone: defaultZone },
        ]) {
            for (const timeFmt of TimeFmts) {
                patterns.push({
                    fmt: `y:M:d ${timeFmt.fmt}${z.fmt}`,
                    zone: z.zone,
                    unsetMilliseconds: timeFmt.unsetMilliseconds,
                });
            }
        }
        return ((_a = this.fromPatterns(text, patterns)) !== null && _a !== void 0 ? _a : 
        // Not found yet? Maybe it's in ISO format? See
        // https://github.com/photostructure/exiftool-vendored.js/issues/71
        this.fromISO(text, defaultZone));
    }
    static fromExifLoose(text, defaultZone) {
        if ((0, String_1.blank)(text))
            return undefined;
        const zone = (0, String_1.notBlank)(defaultZone) ? defaultZone : Timezones_1.UnsetZone;
        // The following are from actual datestamps seen in the wild:
        const formats = [
            "MMM d y H:m:s",
            "MMM d y, H:m:s",
            // Thu Oct 13 00:12:27 2016:
            "ccc MMM d H:m:s y",
        ];
        return this.fromPatterns(text, [
            ...formats.map((fmt) => ({ fmt: fmt + "ZZ" })),
            // And the same formats, without offsets with default zone:
            ...formats.map((fmt) => ({ fmt, zone })),
        ]);
    }
    static fromDateTime(dt, opts) {
        var _a;
        if (dt == null || !dt.isValid || dt.year === 0 || dt.year === 1) {
            return undefined;
        }
        return new ExifDateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.millisecond === 0 && true === (opts === null || opts === void 0 ? void 0 : opts.unsetMilliseconds)
            ? undefined
            : dt.millisecond, dt.offset === Timezones_1.UnsetZoneOffsetMinutes ? undefined : dt.offset, opts === null || opts === void 0 ? void 0 : opts.rawValue, dt.zoneName == null || ((_a = dt.zone) === null || _a === void 0 ? void 0 : _a.name) === Timezones_1.UnsetZone.name
            ? undefined
            : dt.zoneName);
    }
    /**
     * Create an ExifDateTime from a number of milliseconds since the epoch
     * (meaning since 1 January 1970 00:00:00 UTC). Uses the default zone.
     *
     * @param millis - a number of milliseconds since 1970 UTC
     *
     * @param options.rawValue - the original parsed string input
     * @param options.zone - the zone to place the DateTime into. Defaults to 'local'.
     * @param options.locale - a locale to set on the resulting DateTime instance
     * @param options.outputCalendar - the output calendar to set on the resulting DateTime instance
     * @param options.numberingSystem - the numbering system to set on the resulting DateTime instance
     */
    static fromMillis(millis, options = {}) {
        if (options.zone == null ||
            [Timezones_1.UnsetZoneName, Timezones_1.UnsetZone].includes(options.zone)) {
            delete options.zone;
        }
        let dt = luxon_1.DateTime.fromMillis(millis, {
            ...(0, Object_1.omit)(options, "rawValue"),
        });
        if (options.zone == null) {
            dt = dt.setZone(Timezones_1.UnsetZone, { keepLocalTime: true });
        }
        // TODO: is there a way to provide an invalid millisecond value?
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return this.fromDateTime(dt, { rawValue: options.rawValue });
    }
    static now(opts = {}) {
        return this.fromMillis(Date.now(), opts);
    }
    constructor(year, month, day, hour, minute, second, millisecond, tzoffsetMinutes, rawValue, zoneName) {
        this.year = year;
        this.month = month;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
        this.second = second;
        this.millisecond = millisecond;
        this.tzoffsetMinutes = tzoffsetMinutes;
        this.rawValue = rawValue;
        this.zoneName = zoneName;
        _ExifDateTime_dt.set(this, void 0);
    }
    get millis() {
        return this.millisecond;
    }
    get hasZone() {
        return (0, String_1.notBlank)(this.zone);
    }
    get zone() {
        var _a;
        return (_a = this.zoneName) !== null && _a !== void 0 ? _a : (0, Timezones_1.offsetMinutesToZoneName)(this.tzoffsetMinutes);
    }
    setZone(zone, opts) {
        // This is a bit tricky... We want to keep the local time and just _say_ it was in the zone of the image **if we don't already have a zone.**
        // If we _do_ have a zone, assume it was already converted by ExifTool into (probably the system) timezone, which means _don't_ keepLocalTime.
        const dt = this.toDateTime().setZone(zone, {
            keepLocalTime: !this.hasZone,
            ...opts,
        });
        const result = ExifDateTime.fromDateTime(dt, {
            rawValue: this.rawValue,
            unsetMilliseconds: this.millisecond == null,
        });
        // We know this will be defined: this is valid, so changing the zone will
        // also be valid.
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return result;
    }
    /**
     * CAUTION: This instance will inherit the system timezone if this instance
     * has an unset zone
     */
    toDateTime() {
        var _a;
        return (__classPrivateFieldSet(this, _ExifDateTime_dt, (_a = __classPrivateFieldGet(this, _ExifDateTime_dt, "f")) !== null && _a !== void 0 ? _a : luxon_1.DateTime.fromObject({
            year: this.year,
            month: this.month,
            day: this.day,
            hour: this.hour,
            minute: this.minute,
            second: this.second,
            millisecond: this.millisecond,
        }, {
            zone: this.zone,
        }), "f"));
    }
    toEpochSeconds() {
        return this.toDateTime().toUnixInteger();
    }
    toDate() {
        return this.toDateTime().toJSDate();
    }
    toISOString(options = {}) {
        var _a;
        return (0, Maybe_1.denull)(this.toDateTime().toISO({
            suppressMilliseconds: (_a = options.suppressMilliseconds) !== null && _a !== void 0 ? _a : this.millisecond == null,
            includeOffset: this.hasZone && options.includeOffset !== false,
        }));
    }
    toExifString() {
        return (0, DateTime_1.dateTimeToExif)(this.toDateTime(), {
            includeOffset: this.hasZone,
            includeMilliseconds: this.millisecond != null,
        });
    }
    toString() {
        return this.toISOString();
    }
    /**
     * @return the epoch milliseconds of this
     */
    toMillis() {
        return this.toDateTime().toMillis();
    }
    get isValid() {
        return this.toDateTime().isValid;
    }
    toJSON() {
        return {
            _ctor: "ExifDateTime",
            year: this.year,
            month: this.month,
            day: this.day,
            hour: this.hour,
            minute: this.minute,
            second: this.second,
            millisecond: this.millisecond,
            tzoffsetMinutes: this.tzoffsetMinutes,
            rawValue: this.rawValue,
            zoneName: this.zoneName,
        };
    }
    /**
     * @return a new ExifDateTime from the given JSON. Note that this instance **may not be valid**.
     */
    static fromJSON(json) {
        return new ExifDateTime(json.year, json.month, json.day, json.hour, json.minute, json.second, json.millisecond, json.tzoffsetMinutes, json.rawValue, json.zoneName);
    }
    plus(duration) {
        let dt = this.toDateTime().plus(duration);
        if (!this.hasZone) {
            dt = dt.setZone(Timezones_1.UnsetZone, { keepLocalTime: true });
        }
        return ExifDateTime.fromDateTime(dt);
    }
}
exports.ExifDateTime = ExifDateTime;
_ExifDateTime_dt = new WeakMap();
//# sourceMappingURL=ExifDateTime.js.map