"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.WriteTask = exports.DefaultWriteTaskOptions = exports.WriteTaskOptionFields = void 0;
exports.htmlEncode = htmlEncode;
const he_1 = require("he");
const _path = __importStar(require("node:path"));
const Array_1 = require("./Array");
const DateTime_1 = require("./DateTime");
const DefaultExifToolOptions_1 = require("./DefaultExifToolOptions");
const ErrorsAndWarnings_1 = require("./ErrorsAndWarnings");
const ExifToolTask_1 = require("./ExifToolTask");
const FilenameCharsetArgs_1 = require("./FilenameCharsetArgs");
const Number_1 = require("./Number");
const Object_1 = require("./Object");
const Pick_1 = require("./Pick");
const String_1 = require("./String");
const Struct_1 = require("./Struct");
const sep = String.fromCharCode(31); // < unit separator
// this is private because it's very special-purpose for just encoding ExifTool
// WriteTask args:
function htmlEncode(s) {
    return (
    // allowUnsafeSymbols is true because ExifTool doesn't care about &, <, >, ", ', * and `
    (0, he_1.encode)(s, { decimal: true, allowUnsafeSymbols: true })
        // `he` doesn't encode whitespaces (like newlines), but we need that:
        .replace(/\s/g, (m) => (m === " " ? " " : `&#${m.charCodeAt(0)};`)));
}
function enc(o, structValue = false) {
    if (o == null) {
        return "";
    }
    else if ((0, Number_1.isNumber)(o)) {
        return String(o);
    }
    else if (typeof o === "boolean") {
        return o ? "True" : "False";
    }
    else if ((0, String_1.isString)(o)) {
        // Structs need their own escaping here.
        // See https://exiftool.org/struct.html#Serialize
        return htmlEncode(structValue ? o.replace(/[,[\]{}|]/g, (ea) => "|" + ea) : o);
    }
    else if ((0, DateTime_1.isDateOrTime)(o)) {
        return (0, DateTime_1.toExifString)(o);
    }
    else if (Array.isArray(o)) {
        const primitiveArray = o.every((ea) => (0, String_1.isString)(ea) || (0, Number_1.isNumber)(ea));
        return primitiveArray
            ? `${o.map((ea) => enc(ea)).join(sep)}`
            : `[${o.map((ea) => enc(ea)).join(",")}]`;
    }
    else if ((0, Struct_1.isStruct)(o)) {
        // See https://exiftool.org/struct.html#Serialize
        return `{${(0, Object_1.keys)(o)
            .map((k) => enc(k, true) + "=" + enc(o[k], true))
            .join(",")}}`;
    }
    else {
        throw new Error("cannot encode " + JSON.stringify(o));
    }
}
exports.WriteTaskOptionFields = [
    "useMWG",
    "struct",
    "ignoreMinorErrors",
    "writeArgs",
];
exports.DefaultWriteTaskOptions = {
    ...(0, Pick_1.pick)(DefaultExifToolOptions_1.DefaultExifToolOptions, ...exports.WriteTaskOptionFields),
};
/**
 * Task for writing metadata tags to a file.
 */
class WriteTask extends ExifToolTask_1.ExifToolTask {
    sourceFile;
    args;
    options;
    /**
     * @param sourceFile the file to write metadata to
     * @param args the ExifTool arguments
     * @param options task options
     */
    constructor(sourceFile, args, options) {
        super(args, options);
        this.sourceFile = sourceFile;
        this.args = args;
        this.options = options;
    }
    /**
     * Creates a WriteTask for the given file and tags.
     * @param filename the file to write metadata to
     * @param tags the metadata tags to write
     * @param options write task options
     * @returns a new WriteTask instance
     */
    static for(filename, tags, options) {
        const sourceFile = _path.resolve(filename);
        const args = [
            // ensure exiftool thinks this is a write command:
            ...FilenameCharsetArgs_1.Utf8FilenameCharsetArgs,
            `-sep`,
            `${sep}`,
            "-E", // < html encoding https://exiftool.org/faq.html#Q10
        ];
        // "undef" doesn't work: but undef works the same as 2
        args.push("-api", "struct=" + ((0, Number_1.isNumber)(options?.struct) ? options.struct : "2"));
        if (options?.useMWG ?? exports.DefaultWriteTaskOptions.useMWG) {
            args.push("-use", "MWG");
        }
        // Special handling for GPSLatitude and GPSLongitude (due to differences
        // in EXIF, XMP, and MIE encodings). See
        // https://exiftool.org/forum/index.php?topic=14488.0 and
        // https://github.com/photostructure/exiftool-vendored.js/issues/131
        // See https://exiftool.org/TagNames/GPS.html
        if ((0, Number_1.isNumber)(tags.GPSLatitude)) {
            // ExifTool will also accept a number when writing GPSLatitudeRef,
            // positive for north latitudes or negative for south, or a string
            // containing N, North, S or South)
            tags.GPSLatitudeRef ??= tags.GPSLatitude;
        }
        else if (tags.GPSLatitude === null) {
            tags.GPSLatitudeRef ??= null;
        }
        if ((0, Number_1.isNumber)(tags.GPSLongitude)) {
            // ExifTool will also accept a number when writing this tag, positive
            // for east longitudes or negative for west, or a string containing E,
            // East, W or West
            tags.GPSLongitudeRef ??= tags.GPSLongitude;
        }
        else if (tags.GPSLongitude === null) {
            tags.GPSLongitudeRef ??= null;
        }
        if ((0, Number_1.isNumber)(tags.GPSLatitude) && (0, Number_1.isNumber)(tags.GPSLongitude)) {
            // Also set GPSPosition if both GPSLatitude and GPSLongitude are set to
            // give ExifTool a hint that we're happy with it figuring out the correct
            // group for this metadata
            tags.GPSPosition = tags.GPSLatitude + "," + tags.GPSLongitude;
        }
        if ((0, Number_1.isNumber)(tags.GPSAltitude)) {
            // ExifTool will also accept number when writing this tag, with negative
            // numbers indicating below sea level
            tags.GPSAltitudeRef ??= tags.GPSAltitude;
        }
        else if (tags.GPSAltitude === null) {
            tags.GPSAltitudeRef ??= null;
        }
        const fieldsToSet = [];
        for (const key of (0, Object_1.keys)(tags)) {
            const val = tags[key];
            fieldsToSet.push(`-${key}=${enc(val)}`);
        }
        if (fieldsToSet.length === 0) {
            // This is a hack to prevent ExifTool from thinking it should be in "read"
            // mode, and not "write" mode. It _should_ be a no-op, but there are some
            // cases that this can cause ExifTool to behave oddly, so we only apply it
            // if we have no other fields to set.
            fieldsToSet.push("-FileName<FileName");
        }
        args.push(...fieldsToSet, ...(0, Array_1.toArray)(options.writeArgs), sourceFile);
        return new WriteTask(sourceFile, args, options);
    }
    toString() {
        return "WriteTask(" + this.sourceFile + ")";
    }
    // we're handling the stderr output ourselves, so we tell ExifToolTask that
    // all stderr output is not ignorable so we can capture the warnings
    parse(data, error) {
        if (error != null)
            throw error;
        let created = 0;
        let updated = 0;
        let unchanged = 0;
        for (const line of (0, String_1.splitLines)(data)) {
            const m_created = CreatedRE.exec(line)?.groups?.count;
            if (m_created != null) {
                created += (0, Number_1.toInt)(m_created) ?? 0;
                continue;
            }
            // careful! we need to apply UnchangedRE before UpdateRE, as both match
            // "updated"
            const m_unchanged = UnchangedRE.exec(line)?.groups?.count;
            if (m_unchanged != null) {
                unchanged += (0, Number_1.toInt)(m_unchanged) ?? 0;
                continue;
            }
            const m_updated = UpdatedRE.exec(line)?.groups?.count;
            if (m_updated != null) {
                updated += (0, Number_1.toInt)(m_updated) ?? 0;
                continue;
            }
            // if we get here, we didn't match any of the expected patterns.
            this.warnings.push("Unexpected output from ExifTool: " + JSON.stringify(line));
        }
        const w = (0, ErrorsAndWarnings_1.errorsAndWarnings)(this).warnings ?? [];
        return {
            created,
            updated,
            unchanged,
            ...(w.length === 0 ? {} : { warnings: w }),
        };
    }
}
exports.WriteTask = WriteTask;
// "0 files created" | "0 file created" | "    1 image files created"
const CreatedRE = /(?<count>\d{1,5}) [\w ]{1,12}\bcreated$/i;
const UnchangedRE = /(?<count>\d{1,5}) [\w ]{1,12}\b(?:weren't updated|unchanged)$/i;
// "1 image files updated"
const UpdatedRE = /(?<count>\d{1,5}) [\w ]{1,12}\bupdated$/i;
//# sourceMappingURL=WriteTask.js.map