"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var DeviceConfig_exports = {};
__export(DeviceConfig_exports, {
  ConditionalDeviceConfig: () => ConditionalDeviceConfig,
  DeviceConfig: () => DeviceConfig,
  embeddedDevicesDir: () => embeddedDevicesDir,
  generatePriorityDeviceIndex: () => generatePriorityDeviceIndex,
  getDevicesPaths: () => getDevicesPaths,
  loadDeviceIndexInternal: () => loadDeviceIndexInternal,
  loadFulltextDeviceIndexInternal: () => loadFulltextDeviceIndexInternal,
  parseDeviceConfigHash: () => parseHash
});
module.exports = __toCommonJS(DeviceConfig_exports);
var import_config_dir = require("#config_dir");
var import_core = require("@zwave-js/core");
var import_shared = require("@zwave-js/shared");
var import_typeguards = require("alcalzone-shared/typeguards");
var import_json5 = __toESM(require("json5"), 1);
var import_pathe = __toESM(require("pathe"), 1);
var import_gt = __toESM(require("semver/functions/gt.js"), 1);
var import_JsonTemplate = require("../JsonTemplate.js");
var import_utils_safe = require("../utils_safe.js");
var import_AssociationConfig = require("./AssociationConfig.js");
var import_CompatConfig = require("./CompatConfig.js");
var import_ConditionalItem = require("./ConditionalItem.js");
var import_ConditionalPrimitive = require("./ConditionalPrimitive.js");
var import_DeviceMetadata = require("./DeviceMetadata.js");
var import_EndpointConfig = require("./EndpointConfig.js");
var import_ParamInformation = require("./ParamInformation.js");
var import_SceneConfig = require("./SceneConfig.js");
const embeddedDevicesDir = import_pathe.default.join(import_config_dir.configDir, "devices");
const fulltextIndexPath = import_pathe.default.join(embeddedDevicesDir, "fulltext_index.json");
function getDevicesPaths(configDir2) {
  const devicesDir = import_pathe.default.join(configDir2, "devices");
  const indexPath = import_pathe.default.join(devicesDir, "index.json");
  return { devicesDir, indexPath };
}
__name(getDevicesPaths, "getDevicesPaths");
async function hasChangedDeviceFiles(fs, devicesRoot, dir, lastChange) {
  const filesAndDirs = await fs.readDir(dir);
  for (const f of filesAndDirs) {
    const fullPath = import_pathe.default.join(dir, f);
    const stat = await fs.stat(fullPath);
    if ((dir !== devicesRoot || f !== "index.json") && (stat.isFile() || stat.isDirectory()) && stat.mtime > lastChange) {
      return true;
    } else if (stat.isDirectory()) {
      if (await hasChangedDeviceFiles(fs, devicesRoot, fullPath, lastChange)) {
        return true;
      }
    }
  }
  return false;
}
__name(hasChangedDeviceFiles, "hasChangedDeviceFiles");
async function generateIndex(fs, devicesDir, isEmbedded, extractIndexEntries, logger) {
  const index = [];
  (0, import_JsonTemplate.clearTemplateCache)();
  const configFiles = await (0, import_shared.enumFilesRecursive)(fs, devicesDir, (file) => file.endsWith(".json") && !file.endsWith("index.json") && !file.includes("/templates/") && !file.includes("\\templates\\"));
  const fallbackDirs = devicesDir !== embeddedDevicesDir ? [embeddedDevicesDir] : void 0;
  for (const file of configFiles) {
    const relativePath = import_pathe.default.relative(devicesDir, file).replaceAll("\\", "/");
    try {
      const config = await DeviceConfig.from(fs, file, isEmbedded, {
        rootDir: devicesDir,
        fallbackDirs,
        relative: true
      });
      index.push(...extractIndexEntries(config).map((entry) => {
        const ret = {
          ...entry,
          filename: relativePath
        };
        if (devicesDir !== embeddedDevicesDir) {
          ret.rootDir = devicesDir;
        }
        return ret;
      }));
    } catch (e) {
      const message = `Error parsing config file ${relativePath}: ${e.message}`;
      if (process.env.NODE_ENV === "test" || !!(0, import_shared.getenv)("CI")) {
        throw new import_core.ZWaveError(message, import_core.ZWaveErrorCodes.Config_Invalid);
      } else {
        logger?.print(message, "error");
      }
    }
  }
  return index;
}
__name(generateIndex, "generateIndex");
async function loadDeviceIndexShared(fs, devicesDir, indexPath, extractIndexEntries, logger) {
  let needsUpdate = !await (0, import_shared.pathExists)(fs, indexPath);
  let index;
  let mtimeIndex;
  if (!needsUpdate) {
    try {
      const fileContents = await (0, import_shared.readTextFile)(fs, indexPath, "utf8");
      index = import_json5.default.parse(fileContents);
      mtimeIndex = (await fs.stat(indexPath)).mtime;
    } catch {
      logger?.print("Error while parsing index file - regenerating...", "warn");
      needsUpdate = true;
    } finally {
      if (!index) {
        logger?.print("Index file was malformed - regenerating...", "warn");
        needsUpdate = true;
      }
    }
  }
  if (!needsUpdate) {
    needsUpdate = await hasChangedDeviceFiles(fs, devicesDir, devicesDir, mtimeIndex);
    if (needsUpdate) {
      logger?.print("Device configuration files on disk changed - regenerating index...", "verbose");
    }
  }
  if (needsUpdate) {
    index = await generateIndex(fs, devicesDir, true, extractIndexEntries, logger);
    try {
      await (0, import_shared.writeTextFile)(fs, import_pathe.default.join(indexPath), `// This file is auto-generated. DO NOT edit it by hand if you don't know what you're doing!"
${(0, import_shared.stringify)(index, "	")}
`, "utf8");
      logger?.print("Device index regenerated", "verbose");
    } catch (e) {
      logger?.print(`Writing the device index to disk failed: ${e.message}`, "error");
    }
  }
  return index;
}
__name(loadDeviceIndexShared, "loadDeviceIndexShared");
async function generatePriorityDeviceIndex(fs, deviceConfigPriorityDir, logger) {
  return (await generateIndex(fs, deviceConfigPriorityDir, false, (config) => config.devices.map((dev) => ({
    manufacturerId: (0, import_shared.formatId)(config.manufacturerId.toString(16)),
    manufacturer: config.manufacturer,
    label: config.label,
    productType: (0, import_shared.formatId)(dev.productType),
    productId: (0, import_shared.formatId)(dev.productId),
    firmwareVersion: config.firmwareVersion,
    ...config.preferred ? { preferred: true } : {},
    rootDir: deviceConfigPriorityDir
  })), logger)).map(({ filename, ...entry }) => ({
    ...entry,
    // The generated index makes the filenames relative to the given directory
    // but we need them to be absolute
    filename: import_pathe.default.join(deviceConfigPriorityDir, filename)
  }));
}
__name(generatePriorityDeviceIndex, "generatePriorityDeviceIndex");
async function loadDeviceIndexInternal(fs, logger, externalConfigDir) {
  const { devicesDir, indexPath } = getDevicesPaths(externalConfigDir || import_config_dir.configDir);
  return loadDeviceIndexShared(fs, devicesDir, indexPath, (config) => config.devices.map((dev) => ({
    manufacturerId: (0, import_shared.formatId)(config.manufacturerId.toString(16)),
    manufacturer: config.manufacturer,
    label: config.label,
    productType: (0, import_shared.formatId)(dev.productType),
    productId: (0, import_shared.formatId)(dev.productId),
    firmwareVersion: config.firmwareVersion,
    ...config.preferred ? { preferred: true } : {}
  })), logger);
}
__name(loadDeviceIndexInternal, "loadDeviceIndexInternal");
async function loadFulltextDeviceIndexInternal(fs, logger) {
  return loadDeviceIndexShared(fs, embeddedDevicesDir, fulltextIndexPath, (config) => config.devices.map((dev) => ({
    manufacturerId: (0, import_shared.formatId)(config.manufacturerId.toString(16)),
    manufacturer: config.manufacturer,
    label: config.label,
    description: config.description,
    productType: (0, import_shared.formatId)(dev.productType),
    productId: (0, import_shared.formatId)(dev.productId),
    firmwareVersion: config.firmwareVersion,
    ...config.preferred ? { preferred: true } : {},
    rootDir: embeddedDevicesDir
  })), logger);
}
__name(loadFulltextDeviceIndexInternal, "loadFulltextDeviceIndexInternal");
function isHexKeyWith4Digits(val) {
  return typeof val === "string" && import_utils_safe.hexKeyRegex4Digits.test(val);
}
__name(isHexKeyWith4Digits, "isHexKeyWith4Digits");
const firmwareVersionRegex = /^\d{1,3}\.\d{1,3}(\.\d{1,3})?$/;
function isFirmwareVersion(val) {
  return typeof val === "string" && firmwareVersionRegex.test(val) && val.split(".").map((str) => parseInt(str, 10)).every((num) => num >= 0 && num <= 255);
}
__name(isFirmwareVersion, "isFirmwareVersion");
const deflateDict = import_shared.Bytes.from(
  // Substrings appearing in the device config files in descending order of frequency
  // except for very short ones like 0, 1, ...
  // WARNING: THIS MUST NOT BE CHANGED! Doing so breaks decompressing stored hashes.
  [
    `"parameterNumber":`,
    `255`,
    `"value":`,
    `"defaultValue":`,
    `"valueSize":`,
    `"maxValue":`,
    `"minValue":`,
    `"options":`,
    `true`,
    `false`,
    `"allowManualEntry":`,
    `"maxNodes":`,
    `100`,
    `"unsigned":`,
    `"paramInformation":`,
    `"isLifeline":`,
    `"seconds"`,
    `99`,
    `127`,
    `"%"`,
    `65535`,
    `32767`,
    `"minutes"`,
    `"endpoints":`,
    `"hours"`,
    `"multiChannel":`
  ].join(""),
  "utf8"
);
class ConditionalDeviceConfig {
  static {
    __name(this, "ConditionalDeviceConfig");
  }
  static async from(fs, filename, isEmbedded, options) {
    const { relative, rootDir } = options;
    const relativePath = relative ? import_pathe.default.relative(rootDir, filename).replaceAll("\\", "/") : filename;
    const json = await (0, import_JsonTemplate.readJsonWithTemplate)(fs, filename, [
      options.rootDir,
      ...options.fallbackDirs ?? []
    ]);
    return new ConditionalDeviceConfig(relativePath, isEmbedded, json);
  }
  constructor(filename, isEmbedded, definition) {
    this.filename = filename;
    this.isEmbedded = isEmbedded;
    if (!isHexKeyWith4Digits(definition.manufacturerId)) {
      (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
manufacturer id must be a lowercase hexadecimal number with 4 digits`);
    }
    this.manufacturerId = parseInt(definition.manufacturerId, 16);
    for (const prop of ["manufacturer", "label", "description"]) {
      this[prop] = (0, import_ConditionalPrimitive.parseConditionalPrimitive)(filename, "string", prop, definition[prop]);
    }
    if (!(0, import_typeguards.isArray)(definition.devices) || !definition.devices.every((dev) => (0, import_typeguards.isObject)(dev) && isHexKeyWith4Digits(dev.productType) && isHexKeyWith4Digits(dev.productId))) {
      (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
devices is malformed (not an object or type/id that is not a lowercase 4-digit hex key)`);
    }
    this.devices = definition.devices.map(({ productType, productId }) => ({
      productType: parseInt(productType, 16),
      productId: parseInt(productId, 16)
    }));
    if (!(0, import_typeguards.isObject)(definition.firmwareVersion) || !isFirmwareVersion(definition.firmwareVersion.min) || !isFirmwareVersion(definition.firmwareVersion.max)) {
      (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
firmwareVersion is malformed or invalid. Must be x.y or x.y.z where x, y, and z are integers between 0 and 255`);
    } else {
      const { min, max } = definition.firmwareVersion;
      if ((0, import_gt.default)((0, import_shared.padVersion)(min), (0, import_shared.padVersion)(max))) {
        (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
firmwareVersion.min ${min} must not be greater than firmwareVersion.max ${max}`);
      }
      this.firmwareVersion = { min, max };
    }
    if (definition.preferred != void 0 && definition.preferred !== true) {
      (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
preferred must be true or omitted`);
    }
    this.preferred = !!definition.preferred;
    if (definition.endpoints != void 0) {
      const endpoints = /* @__PURE__ */ new Map();
      if (!(0, import_typeguards.isObject)(definition.endpoints)) {
        (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
endpoints is not an object`);
      }
      for (const [key, ep] of Object.entries(definition.endpoints)) {
        if (!/^\d+$/.test(key)) {
          (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
found non-numeric endpoint index "${key}" in endpoints`);
        }
        const epIndex = parseInt(key, 10);
        endpoints.set(epIndex, new import_EndpointConfig.ConditionalEndpointConfig(this, epIndex, ep));
      }
      this.endpoints = endpoints;
    }
    if (definition.associations != void 0) {
      const associations = /* @__PURE__ */ new Map();
      if (!(0, import_typeguards.isObject)(definition.associations)) {
        (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
associations is not an object`);
      }
      for (const [key, assocDefinition] of Object.entries(definition.associations)) {
        if (!/^[1-9][0-9]*$/.test(key)) {
          (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
found non-numeric group id "${key}" in associations`);
        }
        const keyNum = parseInt(key, 10);
        associations.set(keyNum, new import_AssociationConfig.ConditionalAssociationConfig(filename, keyNum, assocDefinition));
      }
      this.associations = associations;
    }
    if (definition.paramInformation != void 0) {
      this.paramInformation = (0, import_ParamInformation.parseConditionalParamInformationMap)(definition, this);
    }
    if (definition.proprietary != void 0) {
      if (!(0, import_typeguards.isObject)(definition.proprietary)) {
        (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
proprietary is not an object`);
      }
      this.proprietary = definition.proprietary;
    }
    if (definition.compat != void 0) {
      if ((0, import_typeguards.isArray)(definition.compat) && definition.compat.every((item) => (0, import_typeguards.isObject)(item))) {
        for (const entry of definition.compat) {
          (0, import_ConditionalItem.validateCondition)(filename, entry, `At least one entry of compat contains an`);
        }
        this.compat = definition.compat.map((item) => new import_CompatConfig.ConditionalCompatConfig(filename, item));
      } else if ((0, import_typeguards.isObject)(definition.compat)) {
        this.compat = new import_CompatConfig.ConditionalCompatConfig(filename, definition.compat);
      } else {
        (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
compat must be an object or any array of conditional objects`);
      }
    }
    if (definition.metadata != void 0) {
      if (!(0, import_typeguards.isObject)(definition.metadata)) {
        (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
metadata is not an object`);
      }
      this.metadata = new import_DeviceMetadata.ConditionalDeviceMetadata(filename, definition.metadata);
    }
    if (definition.scenes != void 0) {
      const scenes = /* @__PURE__ */ new Map();
      if (!(0, import_typeguards.isObject)(definition.scenes)) {
        (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
scenes is not an object`);
      }
      for (const [key, sceneDefinition] of Object.entries(definition.scenes)) {
        if (!/^[1-9][0-9]*$/.test(key)) {
          (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
invalid scene id "${key}" in scenes - must be a positive integer (1-255)`);
        }
        const keyNum = parseInt(key, 10);
        if (keyNum < 1 || keyNum > 255) {
          (0, import_utils_safe.throwInvalidConfig)(`device`, `packages/config/config/devices/${filename}:
scene number ${keyNum} must be between 1 and 255`);
        }
        scenes.set(keyNum, new import_SceneConfig.ConditionalSceneConfig(filename, keyNum, sceneDefinition));
      }
      this.scenes = scenes;
    }
  }
  filename;
  manufacturer;
  manufacturerId;
  label;
  description;
  devices;
  firmwareVersion;
  /** Mark this configuration as preferred over other config files with an overlapping firmware range */
  preferred;
  endpoints;
  associations;
  scenes;
  paramInformation;
  /**
   * Contains manufacturer-specific support information for the
   * ManufacturerProprietary CC
   */
  proprietary;
  /** Contains compatibility options */
  compat;
  /** Contains instructions and other metadata for the device */
  metadata;
  /** Whether this is an embedded configuration or not */
  isEmbedded;
  evaluate(deviceId) {
    return new DeviceConfig(this.filename, this.isEmbedded, (0, import_ConditionalItem.evaluateDeep)(this.manufacturer, deviceId), this.manufacturerId, (0, import_ConditionalItem.evaluateDeep)(this.label, deviceId), (0, import_ConditionalItem.evaluateDeep)(this.description, deviceId), this.devices, this.firmwareVersion, this.preferred, (0, import_ConditionalItem.evaluateDeep)(this.endpoints, deviceId), (0, import_ConditionalItem.evaluateDeep)(this.associations, deviceId), (0, import_ConditionalItem.evaluateDeep)(this.scenes, deviceId), (0, import_ConditionalItem.evaluateDeep)(this.paramInformation, deviceId), this.proprietary, (0, import_ConditionalItem.evaluateDeep)(this.compat, deviceId), (0, import_ConditionalItem.evaluateDeep)(this.metadata, deviceId));
  }
}
class DeviceConfig {
  static {
    __name(this, "DeviceConfig");
  }
  static async from(fs, filename, isEmbedded, options) {
    const ret = await ConditionalDeviceConfig.from(fs, filename, isEmbedded, options);
    return ret.evaluate(options.deviceId);
  }
  constructor(filename, isEmbedded, manufacturer, manufacturerId, label, description, devices, firmwareVersion, preferred, endpoints, associations, scenes, paramInformation, proprietary, compat, metadata) {
    this.filename = filename;
    this.isEmbedded = isEmbedded;
    this.manufacturer = manufacturer;
    this.manufacturerId = manufacturerId;
    this.label = label;
    this.description = description;
    this.devices = devices;
    this.firmwareVersion = firmwareVersion;
    this.preferred = preferred;
    this.endpoints = endpoints;
    this.associations = associations;
    this.scenes = scenes;
    this.paramInformation = paramInformation;
    this.proprietary = proprietary;
    this.compat = compat;
    this.metadata = metadata;
  }
  filename;
  /** Whether this is an embedded configuration or not */
  isEmbedded;
  manufacturer;
  manufacturerId;
  label;
  description;
  devices;
  firmwareVersion;
  /** Mark this configuration as preferred over other config files with an overlapping firmware range */
  preferred;
  endpoints;
  associations;
  scenes;
  paramInformation;
  /**
   * Contains manufacturer-specific support information for the
   * ManufacturerProprietary CC
   */
  proprietary;
  /** Contains compatibility options */
  compat;
  /** Contains instructions and other metadata for the device */
  metadata;
  /** Returns the association config for a given endpoint */
  getAssociationConfigForEndpoint(endpointIndex, group) {
    if (endpointIndex === 0) {
      return this.associations?.get(group) ?? this.endpoints?.get(0)?.associations?.get(group);
    } else {
      return this.endpoints?.get(endpointIndex)?.associations?.get(group);
    }
  }
  getHashable(version) {
    let hashable = {
      // endpoints: {
      // 	associations: {},
      // 	paramInformation: []
      // },
      // proprietary: {},
      // compat: {},
    };
    const sortObject = /* @__PURE__ */ __name((obj) => {
      const ret = {};
      for (const key of Object.keys(obj).toSorted()) {
        ret[key] = obj[key];
      }
      return ret;
    }, "sortObject");
    const cloneAssociationConfig = /* @__PURE__ */ __name((a) => {
      return sortObject((0, import_shared.pick)(a, ["maxNodes", "multiChannel", "isLifeline"]));
    }, "cloneAssociationConfig");
    const cloneAssociationMap = /* @__PURE__ */ __name((target, map) => {
      if (!map || !map.size)
        return;
      target.associations = {};
      for (const [key, value] of map) {
        target.associations[key] = cloneAssociationConfig(value);
      }
      target.associations = sortObject(target.associations);
    }, "cloneAssociationMap");
    const cloneParamInformationMap = /* @__PURE__ */ __name((target, map) => {
      if (!map || !map.size)
        return;
      const getParamKey = /* @__PURE__ */ __name((param) => `${param.parameterNumber}${param.valueBitMask ? `[${(0, import_shared.num2hex)(param.valueBitMask)}]` : ""}`, "getParamKey");
      target.paramInformation = [...map.values()].toSorted((a, b) => getParamKey(a).localeCompare(getParamKey(b))).map((p) => (0, import_shared.cloneDeep)(p));
    }, "cloneParamInformationMap");
    {
      let ep0 = {};
      cloneAssociationMap(ep0, this.associations);
      cloneParamInformationMap(ep0, this.paramInformation);
      ep0 = sortObject(ep0);
      if (Object.keys(ep0).length > 0) {
        hashable.endpoints ??= {};
        hashable.endpoints[0] = ep0;
      }
    }
    if (this.endpoints) {
      for (const [index, endpoint] of this.endpoints) {
        let ep = {};
        cloneAssociationMap(ep, endpoint.associations);
        cloneParamInformationMap(ep, endpoint.paramInformation);
        ep = sortObject(ep);
        if (Object.keys(ep).length > 0) {
          hashable.endpoints ??= {};
          hashable.endpoints[index] = ep;
        }
      }
    }
    if (this.proprietary && Object.keys(this.proprietary).length > 0) {
      hashable.proprietary = sortObject({ ...this.proprietary });
    }
    if (this.compat) {
      let c = {};
      for (const prop of [
        "forceSceneControllerGroupCount",
        "mapRootReportsToEndpoint",
        "mapBasicSet",
        "preserveRootApplicationCCValueIDs",
        "preserveEndpoints",
        "removeEndpoints",
        "treatMultilevelSwitchSetAsEvent"
      ]) {
        if (this.compat[prop] != void 0) {
          c[prop] = this.compat[prop];
        }
      }
      if (this.compat.overrideQueries) {
        c.overrideQueries = Object.fromEntries(this.compat.overrideQueries["overrides"]);
      }
      if (this.compat.addCCs) {
        c.addCCs = Object.fromEntries([...this.compat.addCCs].map(([ccId, def]) => [
          ccId,
          Object.fromEntries(def.endpoints)
        ]));
      }
      if (this.compat.removeCCs) {
        c.removeCCs = Object.fromEntries(this.compat.removeCCs);
      }
      if (this.compat.treatSetAsReport) {
        c.treatSetAsReport = [...this.compat.treatSetAsReport].toSorted();
      }
      c = sortObject(c);
      if (Object.keys(c).length > 0) {
        hashable.compat = c;
      }
    }
    if (version >= 2) {
      for (const ep of Object.values(hashable.endpoints ?? {})) {
        for (const param of ep.paramInformation ?? []) {
          delete param.label;
          delete param.description;
          delete param.$purpose;
          for (const opt of param.options ?? []) {
            delete opt.label;
          }
        }
      }
    }
    if (version < 3) {
      for (const ep of Object.values(hashable.endpoints ?? {})) {
        for (const param of ep.paramInformation ?? []) {
          if ((0, import_typeguards.isArray)(param.allowed) && param.allowed.length === 1 && (0, import_typeguards.isObject)(param.allowed[0])) {
            const allowed = param.allowed[0];
            if (typeof allowed.from === "number" && typeof allowed.to === "number" && (allowed.step == void 0 || allowed.step === 1)) {
              param.minValue = allowed.from;
              param.maxValue = allowed.to;
              delete param.allowed;
            }
          }
        }
      }
    }
    if (version >= 4) {
      if (hashable.endpoints) {
        for (const [key, ep] of Object.entries(hashable.endpoints)) {
          delete ep.paramInformation;
          if (Object.keys(ep).length === 0) {
            delete hashable.endpoints[key];
          }
        }
        if (Object.keys(hashable.endpoints).length === 0) {
          delete hashable.endpoints;
        }
      }
    }
    hashable = sortObject(hashable);
    return hashable;
  }
  /**
   * Returns a hash code that can be used to check whether a device config
   * has changed enough to require a re-interview.
   */
  async getHash(version = DeviceConfig.maxHashVersion) {
    const hashable = this.getHashable(version);
    let hash;
    if (version === 0) {
      const buffer = import_shared.Bytes.from(JSON.stringify(hashable), "utf8");
      return await (0, import_core.digest)("md5", buffer);
    } else if (version === 1) {
      const buffer = import_shared.Bytes.from(JSON.stringify(hashable), "utf8");
      return await (0, import_core.digest)("sha-256", buffer);
    } else {
      hash = (0, import_core.deflateSync)(
        import_shared.Bytes.from(JSON.stringify(hashable), "utf8"),
        // Try to make the hash as small as possible
        { level: 9, dictionary: deflateDict }
      );
    }
    const prefixBytes = import_shared.Bytes.from(`$v${version}$`, "utf8");
    return import_shared.Bytes.concat([prefixBytes, hash]);
  }
  static get maxHashVersion() {
    return 4;
  }
  /**
   * Compares a cached device config hash against the current one.
   * @param currentHash must be at the latest hash version
   * @param cachedHash may be at any version and will be normalized for comparison
   */
  static areHashesEqual(currentHash, cachedHash) {
    const current = parseHash(currentHash);
    const cached = parseHash(cachedHash);
    if (!current || !cached)
      return false;
    if (current.version < 2 && cached.version < 2) {
      return import_shared.Bytes.view(current.hashData).equals(cached.hashData);
    }
    if (current.version < 2 || cached.version < 2) {
      return false;
    }
    let hashable;
    try {
      hashable = JSON.parse(import_shared.Bytes.view((0, import_core.inflateSync)(import_shared.Bytes.view(cached.hashData), { dictionary: deflateDict })).toString("utf8"));
    } catch {
      return false;
    }
    fixBrokenV2Hashable(hashable, cached.version);
    upgradeHashable(hashable, cached.version, current.version);
    const normalizedData = (0, import_core.deflateSync)(import_shared.Bytes.from(JSON.stringify(hashable), "utf8"), { level: 9, dictionary: deflateDict });
    return import_shared.Bytes.view(normalizedData).equals(current.hashData);
  }
}
function parseHash(hash) {
  const hashString = import_shared.Bytes.view(hash).toString("utf8");
  const versionMatch = hashString.match(/^\$v(\d+)\$/);
  if (versionMatch) {
    const version = parseInt(versionMatch[1], 10);
    const hashData = hash.subarray(
      // The prefix is ASCII, so this is safe to do even in the context of UTF-8
      versionMatch[0].length
    );
    return {
      version,
      hashData
    };
  }
  switch (hash.length) {
    case 16:
      return {
        version: 0,
        hashData: hash
      };
    case 32:
      return {
        version: 1,
        hashData: hash
      };
    default:
      return void 0;
  }
}
__name(parseHash, "parseHash");
function fixBrokenV2Hashable(hashable, version) {
  if (version !== 2)
    return;
  for (const ep of Object.values(hashable.endpoints ?? {})) {
    for (const param of ep.paramInformation ?? []) {
      if (param.hidden === false) {
        delete param.hidden;
      }
    }
  }
}
__name(fixBrokenV2Hashable, "fixBrokenV2Hashable");
function upgradeHashable(hashable, fromVersion, targetVersion) {
  if (fromVersion >= targetVersion)
    return;
  if (targetVersion >= 4 && fromVersion < 4 && hashable.endpoints) {
    for (const [key, ep] of Object.entries(hashable.endpoints)) {
      delete ep.paramInformation;
      if (Object.keys(ep).length === 0) {
        delete hashable.endpoints[key];
      }
    }
    if (Object.keys(hashable.endpoints).length === 0) {
      delete hashable.endpoints;
    }
    fromVersion = 4;
  }
}
__name(upgradeHashable, "upgradeHashable");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  ConditionalDeviceConfig,
  DeviceConfig,
  embeddedDevicesDir,
  generatePriorityDeviceIndex,
  getDevicesPaths,
  loadDeviceIndexInternal,
  loadFulltextDeviceIndexInternal,
  parseDeviceConfigHash
});
//# sourceMappingURL=DeviceConfig.js.map
