"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.mapPackagesToFilenameAndBuffer = exports.compareFirmwarePackagesByVersionDesc = exports.parseIntegroFirmwareVersion = exports.resolveInstallableFirmwares = exports.isEncoderConfigurationFirmware = exports.isFirmwareVersionInRange = exports.firmwareVersionSatisfies = exports.removeInvalidBuildMetadataFromFirmwareVersion = exports.isCalibrationFirmware = exports.extractFirmwareVersionFromFirmwarePackageFilename = exports.extractFirmwareId = exports.isFirmwarePackageFilenameCompatibleWithDevice = exports.parseOldFirmwarePackageFilename = exports.extractDeviceComponentsFromOldFirmwarePackageFilename = exports.parseNewFirmwarePackageFilename = exports.isOldFirmwarePackageFilename = exports.isNewFirmwarePackageFilename = exports.parseFirmwareId = exports.oldFirmwarePackageFilenameRegExp = exports.newFirmwarePackageFilenameRegExp = exports.firmwareIdRegExp = void 0;
const hardware_description_1 = require("./hardware-description");
const semver_1 = require("semver");
exports.firmwareIdRegExp = /\d{4}-\d{2}(-\d{4})?(-\d+)*/;
exports.newFirmwarePackageFilenameRegExp = /package_(.*).zip/;
exports.oldFirmwarePackageFilenameRegExp = /package-motion-drive_(.*).zip/;
/**
 * Parses the FWID string and returns a {@link FirmwareId} instance.
 *
 * Special cases:
 * - For product IDs `9506`, `9507`, `9508`, `9606`, `9607`, `9608`, `9612`, and `9614`,
 *   the **CAN** fieldbus protocol is always used, regardless of whether another protocol
 *   is specified in the FWID string.
 *
 * @throws {Error} If the provided FWID does not match the expected pattern.
 */
function parseFirmwareId(fwid) {
    if (!exports.firmwareIdRegExp.test(fwid)) {
        console.warn(`The provided FWID "${fwid}" does not match the expected pattern.`);
        return;
    }
    // Split the FWID string into its dash-separated components.
    const parts = fwid.split('-');
    // Parse the first three components: productId, productVersion, and keyId.
    const [productId, productVersion, keyId] = parts.slice(0, 3).map((part) => parseInt(part));
    // Parse the optional fieldbusProtocol component (if provided).
    // This value is encoded in hexadecimal.
    let fieldbusProtocol = undefined;
    if (parts[3]) {
        fieldbusProtocol = parseInt(parts[3], 16);
    }
    // Special case for CAN-based devices.
    const canProductIds = [9506, 9507, 9508, 9606, 9607, 9608, 9612, 9614];
    if (canProductIds.includes(productId)) {
        fieldbusProtocol = 3; // CAN
    }
    return { productId, productVersion, keyId, fieldbusProtocol };
}
exports.parseFirmwareId = parseFirmwareId;
/**
 * The format for new firmware package filenames is detailed in the "Hardware Description File Format" document.
 */
function isNewFirmwarePackageFilename(filename) {
    return exports.newFirmwarePackageFilenameRegExp.test(filename);
}
exports.isNewFirmwarePackageFilename = isNewFirmwarePackageFilename;
/**
 * Documentation for older firmware package filenames, such as "package-motion-drive_ComEtherCAT-b_CoreC2X-a_Drive400-e_v4.2.1.zip", is unavailable.
 * These package filenames consist of three hardware components (modules): Com, Core, and Drive.
 * Firmware packages with these older naming conventions are specifically built for legacy SOMANET Node devices.
 */
function isOldFirmwarePackageFilename(filename) {
    return exports.oldFirmwarePackageFilenameRegExp.test(filename);
}
exports.isOldFirmwarePackageFilename = isOldFirmwarePackageFilename;
/**
 * Parses a new firmware package filename and returns an instance of {@link NewFirmwarePackageFilename}.
 * Returns `undefined` if the filename does not match the new firmware package filename format.
 *
 * @throws {Error} Throws an error if the provided filename does not match the expected pattern.
 */
function parseNewFirmwarePackageFilename(filename) {
    if (!isNewFirmwarePackageFilename(filename)) {
        throw new Error(`The provided filename "${filename}" does not match the expected pattern.`);
    }
    // 'package_SOMANET-Integro-60_9002-02_motion-drive_v5.4.0-rc.3 (1).zip' -> 'package_SOMANET-Integro-60_9002-02_motion-drive_v5.4.0-rc.3.zip'
    const filenameNoWindowsNumbering = filename.replace(/\s\(\d+\)/, '');
    // 'package_SOMANET-Integro-60_9002-02_motion-drive_v5.4.0-rc.3.zip' -> 'package_SOMANET-Integro-60_9002-02_motion-drive_v5.4.0-rc.3'
    const filenameNoExt = filenameNoWindowsNumbering.replace(/\.[^/.]+$/, '');
    const [description, hardwareName, firmwareId, firmwareName, firmwareVersion] = filenameNoExt.split('_');
    // Attempt to fix firmware version if it contains spaces instead of a plus sign for build metadata.
    // Matthias reported that some customers encountered issues installing package files due to spaces in the version.
    // The firmware version in the package file name does not conform to the valid SemVer specification.
    const fixedFirmwareVersion = firmwareVersion.replace(' ', '+');
    return Object.assign({ description,
        hardwareName,
        firmwareId,
        firmwareName, firmwareVersion: fixedFirmwareVersion }, parseFirmwareId(firmwareId));
}
exports.parseNewFirmwarePackageFilename = parseNewFirmwarePackageFilename;
/**
 * Given an old firmware package filename in the format:
 * "package-motion-drive_ComEtherCAT-b_CoreC2X-a_Drive400-e_v4.2.1.zip",
 * this function extracts the hardware components and returns an array of objects.
 * Each object includes the component name, version, and an empty serial number.
 *
 * This function is useful for determining the compatibility of a firmware package with devices that use an old `stack_info.json` file,
 * which includes only hardware components but lacks a firmware ID. To check if two sets of components match and verify compatibility,
 * use the {@link deviceComponentsMatch} function.
 *
 * @throws {Error} Throws an error if the provided filename does not match the expected pattern.
 */
function extractDeviceComponentsFromOldFirmwarePackageFilename(filename) {
    if (!isOldFirmwarePackageFilename(filename)) {
        throw new Error(`The provided filename "${filename}" does not match the expected pattern.`);
    }
    if (filename.match(/_Com[^_]+_Core[^_]+_Drive/g)) {
        return filename
            .split('_')
            .slice(1, 4)
            .map((b) => {
            // b = ['ComEtherCAT-b', 'CoreC2X-a', 'Drive400-d']
            const p = b.split('-'); // p = ['ComEtherCAT', 'b']
            return {
                name: p[0],
                serialNumber: '',
                version: p[1],
            };
        });
    }
    return [];
}
exports.extractDeviceComponentsFromOldFirmwarePackageFilename = extractDeviceComponentsFromOldFirmwarePackageFilename;
/**
 * Parses an old firmware package filename and returns an instance of {@link OldFirmwarePackageFilename}.
 * Returns `undefined` if the filename does not match the old firmware package filename format.
 *
 * @throws {Error} Throws an error if the provided filename does not match the expected pattern.
 */
function parseOldFirmwarePackageFilename(filename) {
    var _a, _b;
    if (!isOldFirmwarePackageFilename(filename)) {
        throw new Error(`The provided filename "${filename}" does not match the expected pattern.`);
    }
    const parts = filename.split('_');
    const components = extractDeviceComponentsFromOldFirmwarePackageFilename(filename);
    return {
        description: parts[0],
        components,
        firmwareVersion: (_b = (_a = parts === null || parts === void 0 ? void 0 : parts.at(-1)) === null || _a === void 0 ? void 0 : _a.replace('.zip', '')) !== null && _b !== void 0 ? _b : '',
    };
}
exports.parseOldFirmwarePackageFilename = parseOldFirmwarePackageFilename;
/**
 * Checks if a firmware package filename is compatible with a device, based on the provided FWID or a device object with a Hardware Description and optionally an Integro Variant.
 * This function helps determine whether a firmware package can be installed on a device.
 *
 * - If the device includes Stack Info, the old firmware package filename is used.
 * - If an FWID or a device with a Hardware Description is provided, the new firmware package filename is used.
 *
 * @param filename The firmware package filename (either old or new).
 * @param firmwareIdOrDevice The FWID or device object (with `hardwareDescription`, `stackInfo`, and optionally `integroVariant`) to check compatibility.
 * @returns `true` if the firmware package filename is compatible with the device; otherwise, `false`.
 * @throws {Error} Throws an error if an invalid combination is provided, such as using an old firmware package filename with an FWID.
 */
function isFirmwarePackageFilenameCompatibleWithDevice(filename, firmwareIdOrDevice) {
    if (isOldFirmwarePackageFilename(filename)) {
        if (typeof firmwareIdOrDevice === 'string') {
            throw new Error('The old firmware package filename must be checked against the Stack Info for compatibility verification, not the FWID.');
        }
        else if (!firmwareIdOrDevice.stackInfo) {
            throw new Error('The old firmware package filename must be checked against the Stack Info for compatibility verification, not the Hardware Description.');
        }
    }
    if (isNewFirmwarePackageFilename(filename) &&
        typeof firmwareIdOrDevice !== 'string' &&
        !firmwareIdOrDevice.hardwareDescription) {
        throw new Error('The new firmware package filename must be checked against the FWID or Hardware Description for compatibility verification, not the Stack Info.');
    }
    if (typeof firmwareIdOrDevice === 'string') {
        return firmwareIdOrDevice === extractFirmwareId(filename);
    }
    else if (firmwareIdOrDevice.stackInfo) {
        const firmwarePackageFilenameDeviceComponents = extractDeviceComponentsFromOldFirmwarePackageFilename(filename);
        const stackInfoDeviceComponents = (0, hardware_description_1.extractDeviceComponentsFromStackInfo)(firmwareIdOrDevice.stackInfo);
        return (0, hardware_description_1.deviceComponentsMatch)(firmwarePackageFilenameDeviceComponents, stackInfoDeviceComponents);
    }
    else {
        const firmwarePackageFilenameFirmwareId = extractFirmwareId(filename);
        const hardwareDescriptionFirmwareId = extractFirmwareId(firmwareIdOrDevice);
        return firmwarePackageFilenameFirmwareId === hardwareDescriptionFirmwareId;
    }
}
exports.isFirmwarePackageFilenameCompatibleWithDevice = isFirmwarePackageFilenameCompatibleWithDevice;
/**
 * Extracts the Firmware ID (FWID) from either the new firmware package filename or the provided Hardware Description.
 * The optional Integro Variant is used if the Firmware ID includes the Fieldbus Protocol.
 *
 * Note: If additional options from the Integro Variant are incorporated into the FWID in the future, this code will need to be updated accordingly.
 *
 * Note: When a Hardware Description (HD) is provided, the FWID is assembled as follows:
 * - If no assembly is present in the HD, the ID, version, and key ID are taken from the device.
 * - If an assembly is present, the ID and version are taken from the assembly, while the key ID is still taken from the device.
 * - In all cases, the key ID is sourced from the device.
 *
 * @param firmwarePackageFilenameOrDevice The new firmware package filename or a device containing a Hardware Description, with an optional Integro Variant.
 * @throws {Error} If the provided filename does not match the expected pattern.
 * @returns The Firmware ID (FWID), e.g., "9501-01-4622-1".
 */
function extractFirmwareId(firmwarePackageFilenameOrDevice) {
    var _a, _b, _c, _d, _e, _f, _g;
    if (typeof firmwarePackageFilenameOrDevice === 'string') {
        const firmwarePackageFilename = firmwarePackageFilenameOrDevice;
        if (isNewFirmwarePackageFilename(firmwarePackageFilename)) {
            const obj = parseNewFirmwarePackageFilename(firmwarePackageFilename);
            return obj === null || obj === void 0 ? void 0 : obj.firmwareId;
        }
        else {
            throw new Error(`The provided filename "${firmwarePackageFilename}" does not match the expected pattern.`);
        }
    }
    else {
        const device = firmwarePackageFilenameOrDevice;
        let fwid = ((_a = device.hardwareDescription) === null || _a === void 0 ? void 0 : _a.assembly)
            ? `${(_b = device.hardwareDescription) === null || _b === void 0 ? void 0 : _b.assembly.id}-${(_c = device.hardwareDescription) === null || _c === void 0 ? void 0 : _c.assembly.version}`
            : `${(_d = device.hardwareDescription) === null || _d === void 0 ? void 0 : _d.device.id}-${(_e = device.hardwareDescription) === null || _e === void 0 ? void 0 : _e.device.version}`;
        if ((_f = device.hardwareDescription) === null || _f === void 0 ? void 0 : _f.device.keyId) {
            fwid += `-${(_g = device.hardwareDescription) === null || _g === void 0 ? void 0 : _g.device.keyId}`;
        }
        if (device.integroVariant) {
            const ids = device.integroVariant.selectedOptionId['fieldbusProtocol'];
            let fieldbusProtocol = ids[0];
            if (ids.length > 1 && ids.includes(1)) {
                fieldbusProtocol = 1; // EtherCAT is preferred if multiple protocols are selected.
            }
            fwid += `-${fieldbusProtocol}`;
        }
        return fwid;
    }
}
exports.extractFirmwareId = extractFirmwareId;
/**
 * Extracts the firmware version from the given firmware package filename, whether it's in the old or new format.
 *
 * @example
 * "v4.1.0"
 *
 * @throws {Error} Throws an error if the provided filename does not match the expected pattern.
 */
function extractFirmwareVersionFromFirmwarePackageFilename(filename) {
    if (isNewFirmwarePackageFilename(filename)) {
        const parsed = parseNewFirmwarePackageFilename(filename);
        return parsed.firmwareVersion;
    }
    else if (isOldFirmwarePackageFilename(filename)) {
        const parsed = parseOldFirmwarePackageFilename(filename);
        return parsed.firmwareVersion;
    }
    else {
        throw new Error(`The provided filename "${filename}" does not match the expected pattern.`);
    }
}
exports.extractFirmwareVersionFromFirmwarePackageFilename = extractFirmwareVersionFromFirmwarePackageFilename;
/**
 * Older firmware versions for Circulo included a different build to support the Encoder Calibration feature in order to save memory.
 * Newer firmware versions now include the Encoder Calibration feature, so a separate build is no longer necessary.
 *
 * @example
 * "package_SOMANET-Servo-Node-Circulo-7-700_8502-01_motion-drive_v0.4.3-calibration.zip"
 * "v0.4.3-calibration"
 */
function isCalibrationFirmware(version) {
    var _a;
    return (_a = version === null || version === void 0 ? void 0 : version.includes('calibration')) !== null && _a !== void 0 ? _a : false;
}
exports.isCalibrationFirmware = isCalibrationFirmware;
/**
 * Some older firmware versions included invalid build metadata.
 * This function removes any invalid metadata from the provided version.
 */
function removeInvalidBuildMetadataFromFirmwareVersion(version) {
    return version.replace(/-\d+-[A-Za-z0-9]+$/, '');
}
exports.removeInvalidBuildMetadataFromFirmwareVersion = removeInvalidBuildMetadataFromFirmwareVersion;
/**
 * Checks if a firmware version satisfies the given range.
 * If no options are specified, a custom one will be used.
 * Older firmware versions sometimes included invalid build metadata, which could cause the function to fail.
 * To prevent this, the function removes any invalid build metadata before performing the version check.
 */
function firmwareVersionSatisfies(version, range, options = { includePrerelease: true, loose: true }) {
    version = removeInvalidBuildMetadataFromFirmwareVersion(version);
    return (0, semver_1.satisfies)(version, range, options);
}
exports.firmwareVersionSatisfies = firmwareVersionSatisfies;
/**
 * Determines if a firmware version falls within a specified range.
 *
 * @param version The firmware version to check.
 * @param range A version range, following the syntax outlined in {@link https://github.com/npm/node-semver#ranges}.
 * @param calibration If a string, it will be treated as a range. Otherwise, the function returns the boolean value of `calibration`.
 */
function isFirmwareVersionInRange(version, range, calibration) {
    let result = false;
    if (isCalibrationFirmware(version)) {
        result = typeof calibration === 'string' ? firmwareVersionSatisfies(version, calibration) : calibration !== null && calibration !== void 0 ? calibration : false;
    }
    else if (range) {
        result = firmwareVersionSatisfies(version, range);
    }
    return result;
}
exports.isFirmwareVersionInRange = isFirmwareVersionInRange;
/**
 * Checks if the provided firmware version includes the new encoder configuration objects (0x2110 and 0x2112).
 * Firmware versions starting from v5.0.0-rc.5 and v0.4.0-calibration include these objects.
 *
 * Previously, firmware had separate objects for each type of encoder (sensor), resulting in more than 12 record objects,
 * each with several subitems. This consumed unnecessary memory, prompting a refactor to introduce the new encoder configuration objects.
 *
 * The new configuration is referred to as "encoder configuration" (0x2110, 0x2112), while the older is called "sensor configuration".
 */
function isEncoderConfigurationFirmware(version) {
    if (!version) {
        return false;
    }
    if (isCalibrationFirmware(version)) {
        return firmwareVersionSatisfies(version, '>=v0.4.0-calibration');
    }
    else {
        return firmwareVersionSatisfies(version, '>=v5.0.0-rc.5');
    }
}
exports.isEncoderConfigurationFirmware = isEncoderConfigurationFirmware;
/**
 * Resolves a list of installable firmware versions based on the provided firmware package filenames.
 * Filters the list by the specified FWID or device, and optionally sorts the results by version in descending order.
 *
 * @param filenames An array of firmware package filenames to evaluate.
 * @param firmwareIdOrDevice The FWID or device object (with `hardwareDescription`, `stackInfo`, and optionally `integroVariant`) to check compatibility.
 * @param sortByVersion Optional flag to sort the resulting firmware list by version in descending order (default is `false`).
 * @returns A list of compatible firmware packages, each with its filename and version.
 */
function resolveInstallableFirmwares(filenames, firmwareIdOrDevice, sortByVersion = false) {
    return filenames.reduce((firmwares, packageFilename) => {
        try {
            if (isFirmwarePackageFilenameCompatibleWithDevice(packageFilename, firmwareIdOrDevice)) {
                const version = extractFirmwareVersionFromFirmwarePackageFilename(packageFilename);
                firmwares.push({ packageFilename, version });
            }
        }
        catch (err) { }
        return sortByVersion ? firmwares.sort((a, b) => a.version.localeCompare(b.version)).reverse() : firmwares;
    }, []);
}
exports.resolveInstallableFirmwares = resolveInstallableFirmwares;
/**
 * Parses the Integro firmware version and returns an object with the date, day, month, year, version.
 *
 * @param value The Integro firmware version.
 * @returns An object with the date, day, month, year, version.
 */
function parseIntegroFirmwareVersion(value) {
    const str = value.toString();
    const day = str.slice(-2);
    const month = str.slice(-4, -2);
    const year = str.slice(-6, -4);
    const date = new Date(2000 + parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10));
    const version = str.slice(-8, -6);
    return { date, day, month, value, version, year };
}
exports.parseIntegroFirmwareVersion = parseIntegroFirmwareVersion;
/**
 * Compares two firmware versions in descending order.
 */
function compareFirmwarePackagesByVersionDesc(a, b) {
    const re = /_v(.*).zip$/;
    const m1 = a.match(re);
    const m2 = b.match(re);
    if (m1 && m2) {
        return (0, semver_1.rcompare)(m1[1], m2[1]);
    }
    return 0;
}
exports.compareFirmwarePackagesByVersionDesc = compareFirmwarePackagesByVersionDesc;
/**
 * Maps firmware packages to filenames and buffers.
 *
 * @param packages
 * @returns
 */
function mapPackagesToFilenameAndBuffer(packages) {
    return packages.map((pkg) => ({
        filename: pkg.name.substring(pkg.name.lastIndexOf('/') + 1),
        buffer: pkg.buffer,
    }));
}
exports.mapPackagesToFilenameAndBuffer = mapPackagesToFilenameAndBuffer;
//# sourceMappingURL=firmware.js.map