"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
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 _ReadTask_instances, _ReadTask_tagName, _ReadTask_parseTags, _ReadTask_extractLatLon, _ReadTask_latlon, _ReadTask_geoTz, _ReadTask_extractTzOffset, _ReadTask_parseTag;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReadTask = exports.DefaultReadTaskOptions = exports.nullish = void 0;
const batch_cluster_1 = require("batch-cluster");
const _path = __importStar(require("path"));
const BinaryField_1 = require("./BinaryField");
const DefaultExifToolOptions_1 = require("./DefaultExifToolOptions");
const ExifDate_1 = require("./ExifDate");
const ExifDateTime_1 = require("./ExifDateTime");
const ExifTime_1 = require("./ExifTime");
const ExifToolTask_1 = require("./ExifToolTask");
const FilenameCharsetArgs_1 = require("./FilenameCharsetArgs");
const FirstDateTime_1 = require("./FirstDateTime");
const Lazy_1 = require("./Lazy");
const Maybe_1 = require("./Maybe");
const Number_1 = require("./Number");
const Pick_1 = require("./Pick");
const String_1 = require("./String");
const Timezones_1 = require("./Timezones");
/**
 * tag names we don't need to muck with:
 */
const PassthroughTags = [
    "ExifToolVersion",
    "DateStampMode",
    "Sharpness",
    "Firmware",
    "DateDisplayFormat",
];
const NullIsh = ["undef", "null", "undefined"];
function nullish(s) {
    return s == null || ((0, String_1.isString)(s) && NullIsh.includes(s.trim()));
}
exports.nullish = nullish;
exports.DefaultReadTaskOptions = {
    optionalArgs: [],
    ...(0, Pick_1.pick)(DefaultExifToolOptions_1.DefaultExifToolOptions, "numericTags", "useMWG", "includeImageDataMD5", "defaultVideosToUTC", "geoTz"),
};
class ReadTask extends ExifToolTask_1.ExifToolTask {
    constructor(sourceFile, args, options) {
        super(args);
        _ReadTask_instances.add(this);
        this.sourceFile = sourceFile;
        this.args = args;
        this.options = options;
        /** May have keys that are group-prefixed */
        this._raw = {};
        /** Always has non-group-prefixed keys */
        this._tags = {};
        this.invalidLatLon = false;
        _ReadTask_extractLatLon.set(this, (0, Lazy_1.lazy)(() => {
            var _a, _b;
            (_a = this.lat) !== null && _a !== void 0 ? _a : (this.lat = __classPrivateFieldGet(this, _ReadTask_instances, "m", _ReadTask_latlon).call(this, "GPSLatitude", "S", 90));
            (_b = this.lon) !== null && _b !== void 0 ? _b : (this.lon = __classPrivateFieldGet(this, _ReadTask_instances, "m", _ReadTask_latlon).call(this, "GPSLongitude", "W", 180));
            if (this.invalidLatLon) {
                this.lat = this.lon = undefined;
            }
        }));
        _ReadTask_geoTz.set(this, (0, Lazy_1.lazy)(() => {
            __classPrivateFieldGet(this, _ReadTask_extractLatLon, "f").call(this);
            if (this.invalidLatLon || this.lat == null || this.lon == null)
                return;
            try {
                const geoTz = this.options.geoTz(this.lat, this.lon);
                return (0, Timezones_1.normalizeZone)(geoTz);
            }
            catch {
                this.invalidLatLon = true;
                return;
            }
        }));
        this.degroup = this.args.indexOf("-G") !== -1;
        this.tags = { SourceFile: sourceFile };
        this.tags.errors = this.errors;
    }
    static for(filename, options) {
        const opts = { ...exports.DefaultReadTaskOptions, ...options };
        const sourceFile = _path.resolve(filename);
        const args = [
            ...FilenameCharsetArgs_1.Utf8FilenameCharsetArgs,
            "-json",
            "-struct",
            ...opts.optionalArgs,
        ];
        if (opts.useMWG) {
            args.push("-use", "MWG");
        }
        if (opts.includeImageDataMD5) {
            // See https://exiftool.org/forum/index.php?topic=14706.msg79218#msg79218
            args.push("-api", "requesttags=imagedatamd5");
        }
        // IMPORTANT: "-all" must be after numeric tag references (first reference
        // in wins)
        args.push(...opts.numericTags.map((ea) => "-" + ea + "#"));
        // TODO: Do you need -xmp:all, -all, or -all:all? Is -* better?
        args.push("-all", sourceFile);
        return new ReadTask(sourceFile, args, opts);
    }
    toString() {
        return "ReadTask" + this.sourceFile + ")";
    }
    parse(data, err) {
        try {
            this._raw = JSON.parse(data)[0];
        }
        catch (jsonError) {
            // TODO: should restart exiftool?
            (0, batch_cluster_1.logger)().warn("ExifTool.ReadTask(): Invalid JSON", {
                data,
                err,
                jsonError,
            });
            throw err !== null && err !== void 0 ? err : jsonError;
        }
        // ExifTool does "humorous" things to paths, like flip path separators. resolve() undoes that.
        const SourceFile = _path.resolve(this._raw.SourceFile);
        // Sanity check that the result is for the file we want:
        if (SourceFile !== this.sourceFile) {
            // Throw an error rather than add an errors string because this is *really* bad:
            throw new Error(`Internal error: unexpected SourceFile of ${this._raw.SourceFile} for file ${this.sourceFile}`);
        }
        if (this.degroup) {
            Object.keys(this._raw).forEach((keyWithGroup) => {
                this._tags[__classPrivateFieldGet(this, _ReadTask_instances, "m", _ReadTask_tagName).call(this, keyWithGroup)] = this._raw[keyWithGroup];
            });
        }
        else {
            this._tags = this._raw;
        }
        return __classPrivateFieldGet(this, _ReadTask_instances, "m", _ReadTask_parseTags).call(this);
    }
}
exports.ReadTask = ReadTask;
_ReadTask_extractLatLon = new WeakMap(), _ReadTask_geoTz = new WeakMap(), _ReadTask_instances = new WeakSet(), _ReadTask_tagName = function _ReadTask_tagName(k) {
    var _a;
    return this.degroup ? (_a = k.split(":")[1]) !== null && _a !== void 0 ? _a : k : k;
}, _ReadTask_parseTags = function _ReadTask_parseTags() {
    __classPrivateFieldGet(this, _ReadTask_extractLatLon, "f").call(this);
    __classPrivateFieldGet(this, _ReadTask_instances, "m", _ReadTask_extractTzOffset).call(this);
    (0, Maybe_1.map)(this.tz, (ea) => (this.tags.tz = ea));
    (0, Maybe_1.map)(this.tzSource, (ea) => (this.tags.tzSource = ea));
    for (const key of Object.keys(this._raw)) {
        const val = __classPrivateFieldGet(this, _ReadTask_instances, "m", _ReadTask_parseTag).call(this, __classPrivateFieldGet(this, _ReadTask_instances, "m", _ReadTask_tagName).call(this, key), this._raw[key]);
        this.tags[key] = val;
    }
    if (this.errors.length > 0)
        this.tags.errors = this.errors;
    return this.tags;
}, _ReadTask_latlon = function _ReadTask_latlon(tagName, negateRef, maxValid) {
    const tagValue = this._tags[tagName];
    const ref = this._tags[tagName + "Ref"];
    const result = (0, Number_1.toFloat)(tagValue);
    if (result == null) {
        return;
    }
    else if (Math.abs(result) > maxValid) {
        this.errors.push(`Invalid ${tagName}: ${JSON.stringify(tagValue)}`);
        this.invalidLatLon = true;
        return;
    }
    else if ((0, String_1.blank)(ref)) {
        // Videos may not have a GPSLatitudeRef or GPSLongitudeRef: if this is the case, assume the given sign is correct.
        return result;
    }
    else {
        // Versions of ExifTool pre-12 returned properly-negated lat/lon. ExifTool
        // 12+ always returns positive values (!!). Also: if '-GPS*#' is set,
        // we'll see "S" instead of "South", hence the .startsWith() instead of
        // ===:
        const negative = (0, String_1.toS)(ref).toUpperCase().startsWith(negateRef);
        return (negative ? -1 : 1) * Math.abs(result);
    }
}, _ReadTask_extractTzOffset = function _ReadTask_extractTzOffset() {
    (0, Maybe_1.map)((0, Maybe_1.firstDefinedThunk)([
        // If there is an explicit TimeZone tag (which is rare), defer to that
        // before defaulting to UTC for videos:
        () => {
            const tz = (0, Timezones_1.extractTzOffsetFromTags)(this._tags);
            // If this tz offset matches the GPS zone, use the GPS zone name (like "America/Los_Angeles") instead of the offset.
            const z = (0, Timezones_1.normalizeZone)(tz === null || tz === void 0 ? void 0 : tz.tz);
            if (tz != null && z != null) {
                const geoTz = __classPrivateFieldGet(this, _ReadTask_geoTz, "f").call(this);
                if (geoTz != null) {
                    const edt = (0, FirstDateTime_1.firstDateTime)(this._raw);
                    if (edt != null) {
                        const ts = edt.toMillis();
                        const zOffset = z.offset(ts);
                        const geoTzOffset = geoTz.offset(ts);
                        if (zOffset === geoTzOffset) {
                            return {
                                tz: geoTz.name,
                                src: tz.src + " & GPSLatitude/GPSLongitude",
                            };
                        }
                    }
                }
            }
            return tz;
        },
        // See https://github.com/photostructure/exiftool-vendored.js/issues/113
        // Videos are frequently encoded in UTC, but don't include the timezone offset in their datetime stamps.
        // This must be before the tz_lookup/geoTz strategy, as smartphone videos
        // will contain GPS, but still encode timestamps in UTC without an
        // explicit offset. HURRAY
        () => {
            var _a, _b, _c;
            return ((_b = (_a = this._tags) === null || _a === void 0 ? void 0 : _a.MIMEType) === null || _b === void 0 ? void 0 : _b.startsWith("video/")) &&
                this.options.defaultVideosToUTC === true
                ? // If there is a TimeZone tag, defer to that before defaulting to UTC:
                    (_c = (0, Timezones_1.extractTzOffsetFromTags)(this._tags)) !== null && _c !== void 0 ? _c : {
                        tz: "UTC",
                        src: "defaultVideosToUTC",
                    }
                : // not applicable:
                    undefined;
        },
        // If lat/lon is valid, use the tzlookup library, as it will be a proper
        // Zone name (like "America/New_York"), rather than just an hour offset.
        () => (0, Maybe_1.map)(__classPrivateFieldGet(this, _ReadTask_geoTz, "f").call(this), (ea) => ({
            tz: ea.name,
            src: "GPSLatitude/GPSLongitude",
        })),
        // This is a last-ditch estimation heuristic:
        () => (0, Timezones_1.extractTzOffsetFromUTCOffset)(this._tags),
    ]), (ea) => ({ tz: this.tz, src: this.tzSource } = ea));
}, _ReadTask_parseTag = function _ReadTask_parseTag(tagName, value) {
    var _a, _b, _c, _d, _e, _f;
    if (nullish(value))
        return undefined;
    try {
        if (PassthroughTags.indexOf(tagName) >= 0) {
            return value;
        }
        if (Array.isArray(value)) {
            return value.map((ea) => __classPrivateFieldGet(this, _ReadTask_instances, "m", _ReadTask_parseTag).call(this, tagName, ea));
        }
        if (typeof value === "object") {
            const result = {};
            for (const [k, v] of Object.entries(value)) {
                result[k] = __classPrivateFieldGet(this, _ReadTask_instances, "m", _ReadTask_parseTag).call(this, k, v);
            }
            return result;
        }
        if (tagName === "GPSLatitude") {
            return this.lat;
        }
        if (tagName === "GPSLongitude") {
            return this.lon;
        }
        if (typeof value === "string") {
            const b = BinaryField_1.BinaryField.fromRawValue(value);
            if (b != null)
                return b;
            const tz = isUtcTagName(tagName) ? "UTC" : this.tz;
            if (tagName === "When" ||
                tagName.includes("DateTime") ||
                tagName.includes("SubSec") ||
                tagName.toLowerCase().includes("timestamp")) {
                const d = (_a = ExifDateTime_1.ExifDateTime.fromExifStrict(value, tz)) !== null && _a !== void 0 ? _a : ExifDateTime_1.ExifDateTime.fromISO(value, tz);
                if (d != null)
                    return d;
            }
            if (tagName === "When" || tagName.includes("Date")) {
                const d = 
                // Some tags, like CreationDate, actually include time resolution.
                (_f = (_e = (_d = (_c = (_b = ExifDateTime_1.ExifDateTime.fromExifStrict(value, tz)) !== null && _b !== void 0 ? _b : ExifDateTime_1.ExifDateTime.fromISO(value, tz)) !== null && _c !== void 0 ? _c : ExifDateTime_1.ExifDateTime.fromExifLoose(value, tz)) !== null && _d !== void 0 ? _d : ExifDate_1.ExifDate.fromExifStrict(value)) !== null && _e !== void 0 ? _e : ExifDate_1.ExifDate.fromISO(value)) !== null && _f !== void 0 ? _f : ExifDate_1.ExifDate.fromExifLoose(value);
                if (d != null) {
                    return d;
                }
            }
            if (tagName.includes("Time")) {
                const t = ExifTime_1.ExifTime.fromEXIF(value);
                if (t != null)
                    return t;
            }
        }
        // Trust that ExifTool rendered the value with the correct type in JSON:
        return value;
    }
    catch (e) {
        this.addError(`Failed to parse ${tagName} with value ${JSON.stringify(value)}: ${e}`);
        return value;
    }
};
function isUtcTagName(tagName) {
    return tagName.includes("UTC") || tagName.startsWith("GPS");
}
//# sourceMappingURL=ReadTask.js.map