"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractTzOffsetFromUTCOffset = exports.inferLikelyOffsetMinutes = exports.extractTzOffsetFromTags = exports.extractOffset = exports.offsetMinutesToZoneName = exports.reasonableTzOffsetMinutes = exports.normalizeZone = exports.defaultVideosToUTC = exports.UnsetZoneName = exports.UnsetZone = exports.UnsetZoneOffsetMinutes = exports.MaxTzOffsetHours = void 0;
const luxon_1 = require("luxon");
const Array_1 = require("./Array");
const CapturedAtTagNames_1 = require("./CapturedAtTagNames");
const ExifDateTime_1 = require("./ExifDateTime");
const Maybe_1 = require("./Maybe");
const Number_1 = require("./Number");
const String_1 = require("./String");
// Pacific/Kiritimati is +14:00 TIL
// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
exports.MaxTzOffsetHours = 14;
/**
 * Zone instances with this offset are a placeholder for being "unset".
 */
exports.UnsetZoneOffsetMinutes = -1;
/**
 * This is a placeholder for dates where the zone is unknown/unset, because
 * Luxon doesn't officially support "unset" zones.
 */
exports.UnsetZone = luxon_1.Info.normalizeZone(exports.UnsetZoneOffsetMinutes);
/**
 * Zone instances with this name are a placeholder for being "unset".
 */
exports.UnsetZoneName = exports.UnsetZone.name;
/**
 * If `tzSource` matches this value, the tags are from a video, and we had to
 * resort to assuming time fields are in UTC.
 * @see https://github.com/photostructure/exiftool-vendored.js/issues/113
 */
exports.defaultVideosToUTC = "defaultVideosToUTC";
function normalizeZone(z) {
    try {
        if (z == null || (0, String_1.blank)(String(z)))
            return;
        const zone = luxon_1.Info.normalizeZone(z);
        return (zone === null || zone === void 0 ? void 0 : zone.isValid) === true ? zone : undefined;
    }
    catch {
        return;
    }
}
exports.normalizeZone = normalizeZone;
function reasonableTzOffsetMinutes(tzOffsetMinutes) {
    return ((0, Number_1.isNumber)(tzOffsetMinutes) &&
        tzOffsetMinutes !== exports.UnsetZoneOffsetMinutes &&
        Math.abs(tzOffsetMinutes) < exports.MaxTzOffsetHours * 60);
}
exports.reasonableTzOffsetMinutes = reasonableTzOffsetMinutes;
/**
 * Returns a "zone name" (used by `luxon`) that encodes the given offset.
 */
function offsetMinutesToZoneName(offsetMinutes) {
    if (offsetMinutes == null ||
        !(0, Number_1.isNumber)(offsetMinutes) ||
        offsetMinutes === exports.UnsetZoneOffsetMinutes) {
        return undefined;
    }
    if (offsetMinutes === 0)
        return "UTC";
    const sign = offsetMinutes < 0 ? "-" : "+";
    const absMinutes = Math.abs(offsetMinutes);
    if (absMinutes > exports.MaxTzOffsetHours * 60)
        return undefined;
    const hours = Math.floor(absMinutes / 60);
    const minutes = Math.abs(absMinutes % 60);
    // luxon now renders simple hour offsets without padding:
    return `UTC${sign}` + hours + (minutes === 0 ? "" : `:${(0, String_1.pad2)(minutes)}`);
}
exports.offsetMinutesToZoneName = offsetMinutesToZoneName;
function tzHourToOffset(n) {
    return (0, Number_1.isNumber)(n) && reasonableTzOffsetMinutes(n * 60)
        ? offsetMinutesToZoneName(n * 60)
        : undefined;
}
const tzRe = /(?:UTC)?([+-]?)(\d\d?)(?::(\d\d))?/;
/**
 * Parse a timezone offset and return the offset minutes
 */
function extractOffset(tz) {
    if (tz == null || (0, String_1.blank)(tz)) {
        return undefined;
    }
    if ((0, String_1.isString)(tz) && luxon_1.Info.isValidIANAZone(tz)) {
        return { tz, src: "validIANAZone" };
    }
    return (0, Maybe_1.map)(tzRe.exec(tz), (m) => {
        var _a;
        return (0, Maybe_1.map)(offsetMinutesToZoneName((m[1] === "-" ? -1 : 1) *
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            (parseInt(m[2]) * 60 + parseInt((_a = m[3]) !== null && _a !== void 0 ? _a : "0"))), (ea) => ({ tz: ea, src: "offsetMinutesToZoneName" }));
    });
}
exports.extractOffset = extractOffset;
function extractTzOffsetFromTags(t) {
    return (0, Maybe_1.firstDefinedThunk)([
        () => (0, Maybe_1.first)([
            "TimeZone",
            "OffsetTime",
            "OffsetTimeOriginal",
            "OffsetTimeDigitized",
            "TimeZoneOffset",
        ], (tagName) => (0, Maybe_1.map)(extractOffset(t[tagName]), (ea) => ({
            tz: ea.tz,
            src: ea.src + " from " + tagName,
        }))),
        () => (0, Maybe_1.map)(t.TimeZoneOffset, (value) => (0, Maybe_1.map)(tzHourToOffset(Array.isArray(value) ? value[0] : value), (tz) => ({
            tz,
            src: "TimeZoneOffset",
        }))),
    ]);
}
exports.extractTzOffsetFromTags = extractTzOffsetFromTags;
// timezone offsets may be on a 15 minute boundary, but if GPS acquisition is
// old, this can be spurious. We get less mistakes with a larger multiple, so
// we're using 30 minutes instead of 15. See
// https://www.timeanddate.com/time/time-zones-interesting.html
const TzBoundaryMinutes = 30;
function inferLikelyOffsetMinutes(deltaMinutes) {
    return TzBoundaryMinutes * Math.floor(deltaMinutes / TzBoundaryMinutes);
}
exports.inferLikelyOffsetMinutes = inferLikelyOffsetMinutes;
function extractTzOffsetFromUTCOffset(t) {
    var _a;
    const gpsStamps = (0, Array_1.compact)([t.GPSDateStamp, t.GPSTimeStamp]);
    if (gpsStamps.length === 2) {
        (_a = t.GPSDateTimeStamp) !== null && _a !== void 0 ? _a : (t.GPSDateTimeStamp = gpsStamps.join(" "));
    }
    // We can always assume these are in UTC:
    const utc = (0, Maybe_1.first)(["GPSDateTime", "DateTimeUTC", "GPSDateTimeStamp"], (tagName) => {
        var _a;
        const edt = ExifDateTime_1.ExifDateTime.fromExifStrict(t[tagName]);
        const s = edt != null && (edt.zone == null || edt.zone === "UTC")
            ? (_a = edt.setZone("UTC", { keepLocalTime: true })) === null || _a === void 0 ? void 0 : _a.toEpochSeconds()
            : undefined;
        return s != null
            ? {
                tagName,
                s,
            }
            : undefined;
    });
    if (utc == null)
        return;
    // If we can find any of these without a zone, the timezone should be the
    // offset between this time and the GPS time.
    const dt = (0, Maybe_1.first)(CapturedAtTagNames_1.CapturedAtTagNames, (tagName) => {
        var _a;
        const edt = ExifDateTime_1.ExifDateTime.fromExifStrict(t[tagName]);
        const s = edt != null && edt.zone == null
            ? (_a = edt.setZone("UTC", { keepLocalTime: true })) === null || _a === void 0 ? void 0 : _a.toEpochSeconds()
            : undefined;
        return s != null
            ? {
                tagName,
                s,
            }
            : undefined;
    });
    if (dt == null)
        return;
    const diffSeconds = dt.s - utc.s;
    const offsetMinutes = inferLikelyOffsetMinutes(diffSeconds / 60);
    return (0, Maybe_1.map)(offsetMinutesToZoneName(offsetMinutes), (tz) => ({
        tz,
        src: `offset between ${dt.tagName} and ${utc.tagName}`,
    }));
}
exports.extractTzOffsetFromUTCOffset = extractTzOffsetFromUTCOffset;
//# sourceMappingURL=Timezones.js.map