"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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReadTask = exports.DefaultReadTaskOptions = exports.ReadTaskOptionFields = void 0;
exports.nullish = nullish;
const batch_cluster_1 = require("batch-cluster");
const _path = __importStar(require("node:path"));
const Array_1 = require("./Array");
const BinaryField_1 = require("./BinaryField");
const Boolean_1 = require("./Boolean");
const DefaultExifToolOptions_1 = require("./DefaultExifToolOptions");
const ErrorsAndWarnings_1 = require("./ErrorsAndWarnings");
const ExifDate_1 = require("./ExifDate");
const ExifDateTime_1 = require("./ExifDateTime");
const ExifTime_1 = require("./ExifTime");
const ExifToolOptions_1 = require("./ExifToolOptions");
const ExifToolTask_1 = require("./ExifToolTask");
const File_1 = require("./File");
const FilenameCharsetArgs_1 = require("./FilenameCharsetArgs");
const GPS_1 = require("./GPS");
const Lazy_1 = require("./Lazy");
const Number_1 = require("./Number");
const Object_1 = require("./Object");
const OnlyZerosRE_1 = require("./OnlyZerosRE");
const Pick_1 = require("./Pick");
const String_1 = require("./String");
const Timezones_1 = require("./Timezones");
/**
 * tag names we don't need to muck with, but name conventions (like including
 * "date") suggest they might be date/time tags
 */
const PassthroughTags = [
    "ExifToolVersion",
    "DateStampMode",
    "Sharpness",
    "Firmware",
    "DateDisplayFormat",
];
exports.ReadTaskOptionFields = [
    "adjustTimeZoneIfDaylightSavings",
    "backfillTimezones",
    "defaultVideosToUTC",
    "geolocation",
    "geoTz",
    "ignoreMinorErrors",
    "ignoreZeroZeroLatLon",
    "imageHashType",
    "includeImageDataMD5",
    "inferTimezoneFromDatestamps",
    "inferTimezoneFromDatestampTags",
    "inferTimezoneFromTimeStamp",
    "keepUTCTime",
    "numericTags",
    "preferTimezoneInferenceFromGps",
    "readArgs",
    "struct",
    "useMWG",
];
const NullIsh = ["undef", "null", "undefined"];
function nullish(s) {
    return s == null || ((0, String_1.isString)(s) && NullIsh.includes(s.trim()));
}
exports.DefaultReadTaskOptions = {
    ...(0, Pick_1.pick)(DefaultExifToolOptions_1.DefaultExifToolOptions, ...exports.ReadTaskOptionFields),
};
const MaybeDateOrTimeRe = /when|date|time|subsec|creat|modif/i;
class ReadTask extends ExifToolTask_1.ExifToolTask {
    sourceFile;
    args;
    options;
    degroup;
    #raw = {};
    #rawDegrouped = {};
    #tags = {};
    /**
     * @param sourceFile the file to read
     * @param args the full arguments to pass to exiftool that take into account
     * the flags in `options`
     * @param options the required options for reading metadata
     */
    constructor(sourceFile, args, options) {
        super(args, options);
        this.sourceFile = sourceFile;
        this.args = args;
        this.options = options;
        // See https://github.com/photostructure/exiftool-vendored.js/issues/147#issuecomment-1642580118
        this.degroup = this.args.includes("-G");
        this.#tags = { SourceFile: sourceFile };
        this.#tags.errors = this.errors;
    }
    static for(filename, options) {
        const opts = (0, ExifToolOptions_1.handleDeprecatedOptions)({
            ...exports.DefaultReadTaskOptions,
            ...options,
        });
        const sourceFile = _path.resolve(filename);
        const args = [
            ...FilenameCharsetArgs_1.Utf8FilenameCharsetArgs,
            "-json",
            ...(0, Array_1.toArray)(opts.readArgs),
        ];
        // "-api struct=undef" doesn't work: but it's the same as struct=0:
        args.push("-api", "struct=" + ((0, Number_1.isNumber)(opts.struct) ? opts.struct : "0"));
        if (opts.useMWG) {
            args.push("-use", "MWG");
        }
        if (opts.imageHashType != null && opts.imageHashType !== false) {
            // See https://exiftool.org/forum/index.php?topic=14706.msg79218#msg79218
            args.push("-api", "requesttags=imagedatahash");
            args.push("-api", "imagehashtype=" + opts.imageHashType);
        }
        if (true === opts.geolocation) {
            args.push("-api", "geolocation");
        }
        if (true === opts.keepUTCTime) {
            args.push("-api", "keepUTCTime");
        }
        // IMPORTANT: "-all" must be after numeric tag references, as the first
        // reference in wins
        args.push(...opts.numericTags.map((ea) => "-" + ea + "#"));
        // We have to add a -all or else we'll only get the numericTags. sad.
        // 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 + ")";
    }
    // only exposed for tests
    parse(data, err) {
        try {
            // Fix ExifToolVersion to be a string to preserve version distinctions like 12.3 vs 12.30
            const versionFixedData = data.replace(/"ExifToolVersion"\s*:\s*(\d+(?:\.\d+)?)/, '"ExifToolVersion":"$1"');
            this.#raw = JSON.parse(versionFixedData)[0];
        }
        catch (jsonError) {
            // TODO: should restart exiftool?
            (0, batch_cluster_1.logger)().warn("ExifTool.ReadTask(): Invalid JSON", {
                data,
                err,
                jsonError,
            });
            throw err ?? jsonError;
        }
        // ExifTool does "humorous" things to paths, like flip path separators. resolve() undoes that.
        if ((0, String_1.notBlank)(this.#raw.SourceFile)) {
            if (!(0, File_1.compareFilePaths)(this.#raw.SourceFile, this.sourceFile)) {
                // this would indicate a bug in batch-cluster:
                throw new Error(`Internal error: unexpected SourceFile of ${this.#raw.SourceFile} for file ${this.sourceFile}`);
            }
        }
        return this.#parseTags();
    }
    #isVideo() {
        return String(this.#rawDegrouped?.MIMEType).startsWith("video/");
    }
    #defaultToUTC() {
        return this.#isVideo() && this.options.defaultVideosToUTC;
    }
    #tagName(k) {
        return this.degroup ? (k.split(":")[1] ?? k) : k;
    }
    #parseTags() {
        if (this.degroup) {
            this.#rawDegrouped = {};
            for (const [key, value] of Object.entries(this.#raw)) {
                const k = this.#tagName(key);
                this.#rawDegrouped[k] = value;
            }
        }
        else {
            this.#rawDegrouped = this.#raw;
        }
        // avoid casting `this.tags as any` for the rest of the function:
        const tags = this.#tags;
        // Must be run before extracting tz offset, to repair possibly-invalid
        // GeolocationTimeZone
        this.#extractGpsMetadata();
        const tzSrc = this.#extractTzOffset();
        if (tzSrc) {
            tags.zone = tzSrc.zone;
            tags.tz = tzSrc.tz;
            tags.tzSource = tzSrc.src;
        }
        for (const [key, value] of Object.entries(this.#raw)) {
            const k = this.#tagName(key);
            // Did something already set this? (like GPS tags)
            if (key in tags)
                continue;
            const v = this.#parseTag(k, value);
            // Note that we set `key` (which may include a group prefix):
            if (v == null) {
                // Use Reflect.deleteProperty for dynamic keys
                Reflect.deleteProperty(tags, key);
            }
            else {
                tags[key] = v;
            }
        }
        // we could `return {...tags, ...errorsAndWarnings(this, tags)}` but tags is
        // a chonky monster, and we don't want to double the work for the poor
        // garbage collector.
        const { errors, warnings } = (0, ErrorsAndWarnings_1.errorsAndWarnings)(this, tags);
        tags.errors = errors;
        tags.warnings = warnings;
        return tags;
    }
    #extractGpsMetadata = (0, Lazy_1.lazy)(() => {
        const result = (0, GPS_1.parseGPSLocation)(this.#rawDegrouped, this.options);
        if (result?.warnings != null && (result.warnings.length ?? 0) > 0) {
            this.warnings.push(...result.warnings);
        }
        if (result?.invalid !== true) {
            for (const [k, v] of Object.entries(result?.result ?? {})) {
                this.#tags[k] = v;
            }
        }
        return result;
    });
    #gpsIsInvalid = (0, Lazy_1.lazy)(() => this.#extractGpsMetadata()?.invalid ?? false);
    #gpsResults = (0, Lazy_1.lazy)(() => this.#gpsIsInvalid() ? {} : (this.#extractGpsMetadata()?.result ?? {}));
    #extractTzOffsetFromGps = (0, Lazy_1.lazy)(() => {
        const gps = this.#extractGpsMetadata();
        const lat = gps?.result?.GPSLatitude;
        const lon = gps?.result?.GPSLongitude;
        if (gps == null || gps.invalid === true || lat == null || lon == null)
            return;
        // First try GeolocationTimeZone:
        const geolocZone = (0, Timezones_1.normalizeZone)(this.#rawDegrouped.GeolocationTimeZone);
        if (geolocZone != null) {
            return {
                zone: geolocZone.name,
                tz: geolocZone.name,
                src: "GeolocationTimeZone",
            };
        }
        try {
            const geoTz = this.options.geoTz(lat, lon);
            const zone = (0, Timezones_1.normalizeZone)(geoTz);
            if (zone != null) {
                return {
                    zone: zone.name,
                    tz: zone.name,
                    src: "GPSLatitude/GPSLongitude",
                };
            }
        }
        catch (error) {
            this.warnings.push("Failed to determine timezone from GPS coordinates: " + error);
        }
        return;
    });
    #tz = (0, Lazy_1.lazy)(() => this.#extractTzOffset()?.tz);
    #extractTzOffset = (0, Lazy_1.lazy)(() => {
        if (true === this.options.preferTimezoneInferenceFromGps) {
            const fromGps = this.#extractTzOffsetFromGps();
            if (fromGps != null) {
                return fromGps;
            }
        }
        return ((0, Timezones_1.extractTzOffsetFromTags)(this.#rawDegrouped, this.options) ??
            this.#extractTzOffsetFromGps() ??
            (0, Timezones_1.extractTzOffsetFromDatestamps)(this.#rawDegrouped, this.options) ??
            // See https://github.com/photostructure/exiftool-vendored.js/issues/113
            // and https://github.com/photostructure/exiftool-vendored.js/issues/156
            // Videos are frequently encoded in UTC, but don't include the
            // timezone offset in their datetime stamps.
            (this.#defaultToUTC()
                ? {
                    zone: "UTC",
                    tz: "UTC",
                    src: "defaultVideosToUTC",
                }
                : // not applicable:
                    undefined) ??
            // This is a last-ditch estimation heuristic:
            (0, Timezones_1.extractTzOffsetFromUTCOffset)(this.#rawDegrouped) ??
            // No, really, this is the even worse than UTC offset heuristics:
            (0, Timezones_1.extractTzOffsetFromTimeStamp)(this.#rawDegrouped, this.options));
    });
    #parseTag(tagName, value) {
        if (nullish(value))
            return undefined;
        try {
            if (PassthroughTags.indexOf(tagName) >= 0) {
                return value;
            }
            if (tagName.startsWith("GPS") || tagName.startsWith("Geolocation")) {
                if (this.#gpsIsInvalid()) {
                    // No GPS data should be trusted!
                    return undefined;
                }
                const gpsResult = this.#gpsResults()?.[tagName];
                if (gpsResult != null) {
                    // If we parsed out a better value, use that:
                    return gpsResult;
                }
                // Otherwise, parse the raw value like any other tag: (It might be
                // something like "GPSTimeStamp"):
            }
            if (Array.isArray(value)) {
                return value.map((ea) => this.#parseTag(tagName, ea));
            }
            if ((0, Object_1.isObject)(value)) {
                const result = {};
                for (const [k, v] of Object.entries(value)) {
                    result[k] = this.#parseTag(tagName + "." + k, v);
                }
                return result;
            }
            if (typeof value === "string") {
                const b = BinaryField_1.BinaryField.fromRawValue(value);
                if (b != null)
                    return b;
                if (/Valid$/.test(tagName)) {
                    const b = (0, Boolean_1.toBoolean)(value);
                    if (b != null)
                        return b;
                }
                if (MaybeDateOrTimeRe.test(tagName) &&
                    // Reject date/time keys that are "0" or "00" (found in Canon
                    // SubSecTime values)
                    !OnlyZerosRE_1.OnlyZerosRE.test(value)) {
                    // if #defaultToUTC() is true, _we actually think zoneless
                    // datestamps are all in UTC_, rather than being in `this.tz` (which
                    // may be from GPS or other heuristics). See issue #153.
                    const tz = isUtcTagName(tagName) || this.#defaultToUTC()
                        ? "UTC"
                        : this.options.backfillTimezones
                            ? this.#tz()
                            : undefined;
                    // Time-only tags have "time" but not "date" in their name:
                    const keyIncludesTime = /subsec|time/i.test(tagName);
                    const keyIncludesDate = /date/i.test(tagName);
                    const keyIncludesWhen = /when/i.test(tagName); // < ResourceEvent.When
                    const result = (keyIncludesTime || keyIncludesDate || keyIncludesWhen
                        ? ExifDateTime_1.ExifDateTime.from(value, tz)
                        : undefined) ??
                        (keyIncludesTime || keyIncludesWhen
                            ? ExifTime_1.ExifTime.fromEXIF(value, tz)
                            : undefined) ??
                        (keyIncludesDate || keyIncludesWhen
                            ? ExifDate_1.ExifDate.from(value)
                            : undefined) ??
                        value;
                    const defaultTz = this.#tz();
                    if (this.options.backfillTimezones &&
                        result != null &&
                        defaultTz != null &&
                        result instanceof ExifDateTime_1.ExifDateTime &&
                        this.#defaultToUTC() &&
                        !isUtcTagName(tagName) &&
                        true === result.inferredZone) {
                        return result.setZone(defaultTz);
                    }
                    return result;
                }
            }
            // Trust that ExifTool rendered the value with the correct type in JSON:
            return value;
        }
        catch (e) {
            this.warnings.push(`Failed to parse ${tagName} with value ${JSON.stringify(value)}: ${e}`);
            return value;
        }
    }
}
exports.ReadTask = ReadTask;
function isUtcTagName(tagName) {
    return tagName.includes("UTC") || tagName.startsWith("GPS");
}
//# sourceMappingURL=ReadTask.js.map