"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 __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
var __typeError = (msg) => {
  throw TypeError(msg);
};
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 __using = (stack, value, async) => {
  if (value != null) {
    if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
    var dispose, inner;
    if (async) dispose = value[__knownSymbol("asyncDispose")];
    if (dispose === void 0) {
      dispose = value[__knownSymbol("dispose")];
      if (async) inner = dispose;
    }
    if (typeof dispose !== "function") __typeError("Object not disposable");
    if (inner) dispose = function() {
      try {
        inner.call(this);
      } catch (e) {
        return Promise.reject(e);
      }
    };
    stack.push([async, dispose, value]);
  } else if (async) {
    stack.push([async]);
  }
  return value;
};
var __callDispose = (stack, error, hasError) => {
  var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
    return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
  };
  var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
  var next = (it) => {
    while (it = stack.pop()) {
      try {
        var result = it[1] && it[1].call(it[2]);
        if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
      } catch (e) {
        fail(e);
      }
    }
    if (hasError) throw error;
  };
  return next();
};
var NodeJsUdpChannel_exports = {};
__export(NodeJsUdpChannel_exports, {
  NodeJsUdpChannel: () => NodeJsUdpChannel
});
module.exports = __toCommonJS(NodeJsUdpChannel_exports);
var import_general = require("@matter/general");
var dgram = __toESM(require("node:dgram"), 1);
var import_NodeJsNetwork = require("./NodeJsNetwork.js");
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
const logger = import_general.Logger.get("NodejsChannel");
const UDP_SEND_TIMEOUT_CHECK_INTERVAL = import_general.Seconds.one;
function createDgramSocket(host, port, options) {
  const socket = dgram.createSocket(options);
  return new Promise((resolve, reject) => {
    const handleBindError = (error) => {
      try {
        socket.close();
      } catch (error2) {
        logger.debug("Error closing socket:", error2);
      }
      const code = error?.code;
      let desc = `${host ? host : options.type === "udp4" ? "0.0.0.0" : "{::}"}`;
      if (port !== void 0) {
        desc = `${desc}:${port}`;
      }
      switch (code) {
        case "EADDRINUSE":
          error = new import_general.AddressInUseError(`Cannot bind ${desc} because port is already in use`);
          break;
        default:
          error = new import_general.BindError(`Cannot bind to ${desc} (code ${code})`);
          break;
      }
      reject(error);
    };
    socket.on("error", handleBindError);
    socket.bind(port, host, () => {
      const { address: localHost, port: localPort } = socket.address();
      logger.debug(
        "Socket created and bound ",
        import_general.Diagnostic.dict({
          remoteAddress: `${host}:${port}`,
          localAddress: `${localHost}:${localPort}`
        })
      );
      socket.removeListener("error", handleBindError);
      socket.on("error", (error) => logger.error(error));
      resolve(socket);
    });
  });
}
class NodeJsUdpChannel {
  #lifetime;
  #type;
  #socket;
  #netInterface;
  #observedInterface;
  static async create({
    lifetime: lifetimeOwner,
    listeningPort,
    type,
    listeningAddress,
    netInterface,
    reuseAddress
  }) {
    var _stack2 = [];
    try {
      const name = `${listeningAddress?.includes(":") ? `[${listeningAddress}]` : listeningAddress ?? "*"}:${listeningPort}`;
      const lifetime = __using(_stack2, (lifetimeOwner ?? import_general.Lifetime.process).join("socket", import_general.Diagnostic.strong(name)));
      lifetime.details.intf = netInterface;
      let dgramType;
      switch (type) {
        case "udp":
        case "udp6":
          dgramType = "udp6";
          break;
        case "udp4":
          dgramType = "udp4";
          break;
        default:
          throw new import_general.ImplementationError(`Unrecognized UDP socket type ${type}`);
      }
      const socketOptions = { type: dgramType };
      if (type === "udp6") {
        socketOptions.ipv6Only = true;
      }
      if (reuseAddress) {
        socketOptions.reuseAddr = true;
      }
      let socket;
      {
        var _stack = [];
        try {
          const _creating = __using(_stack, lifetime.join("creating"));
          socket = await createDgramSocket(listeningAddress, listeningPort, socketOptions);
        } catch (_) {
          var _error = _, _hasError = true;
        } finally {
          __callDispose(_stack, _error, _hasError);
        }
      }
      socket.setBroadcast(true);
      let netInterfaceZone;
      if (netInterface === void 0 && listeningAddress !== void 0) {
        netInterfaceZone = import_NodeJsNetwork.NodeJsNetwork.getNetInterfaceForIp(listeningAddress) || void 0;
      }
      if (netInterface !== void 0) {
        netInterfaceZone = import_NodeJsNetwork.NodeJsNetwork.getNetInterfaceZoneIpv6(netInterface);
        let multicastInterface;
        if (type === "udp4") {
          multicastInterface = import_NodeJsNetwork.NodeJsNetwork.getMulticastInterfaceIpv4(netInterface);
          if (multicastInterface === void 0) {
            throw new import_general.NoAddressAvailableError(`No IPv4 addresses on interface "${netInterface}"`);
          }
        } else {
          if (netInterfaceZone === void 0) {
            throw new import_general.NoAddressAvailableError(`No IPv6 addresses on interface "${netInterface}"`);
          }
          multicastInterface = `::%${netInterfaceZone}`;
        }
        logger.debug(
          "Initialize multicast",
          import_general.Diagnostic.dict({
            address: `${multicastInterface}:${listeningPort}`,
            interface: netInterface,
            type
          })
        );
        socket.setMulticastInterface(multicastInterface);
      }
      return new NodeJsUdpChannel(lifetime, type, socket, netInterfaceZone);
    } catch (_2) {
      var _error2 = _2, _hasError2 = true;
    } finally {
      __callDispose(_stack2, _error2, _hasError2);
    }
  }
  maxPayloadSize = import_general.MAX_UDP_MESSAGE_SIZE;
  /**
   * Timer for a maximum interval to check for dangling send calls that are not completed.
   * The way it is implemented we ensure that any "send" is rejected latest after < 2s
   */
  #sendTimer = import_general.Time.getTimer(
    "UDPChannel.send timeout check",
    UDP_SEND_TIMEOUT_CHECK_INTERVAL,
    () => this.#rejectDanglingSends()
  );
  #sendsInProgress = /* @__PURE__ */ new Map();
  constructor(lifetime, type, socket, netInterface) {
    this.#lifetime = lifetime;
    this.#type = type;
    this.#socket = socket;
    this.#netInterface = netInterface;
  }
  addMembership(membershipAddress) {
    const multicastInterfaces = import_NodeJsNetwork.NodeJsNetwork.getMembershipMulticastInterfaces(
      this.#netInterface,
      this.#type === "udp4"
    );
    for (const multicastInterface of multicastInterfaces) {
      try {
        this.#socket.addMembership(membershipAddress, multicastInterface);
      } catch (error) {
        logger.warn(
          `Error adding membership for address ${membershipAddress}${multicastInterface ? ` with interface ${multicastInterface}` : ""}: ${error}`
        );
      }
    }
  }
  dropMembership(membershipAddress) {
    const multicastInterfaces = import_NodeJsNetwork.NodeJsNetwork.getMembershipMulticastInterfaces(
      this.#netInterface,
      this.#type === "udp4"
    );
    for (const multicastInterface of multicastInterfaces) {
      try {
        this.#socket.dropMembership(membershipAddress, multicastInterface);
      } catch (error) {
        logger.warn(
          `Error removing membership for address ${membershipAddress}${multicastInterface ? ` with interface ${multicastInterface}` : ""}: ${error}`
        );
      }
    }
  }
  onData(listener) {
    const messageListener = (data, { address, port }) => {
      const netInterface = this.#netInterface ?? import_NodeJsNetwork.NodeJsNetwork.getNetInterfaceForIp(address);
      if (netInterface && this.#observedInterface === void 0) {
        this.#observedInterface = netInterface;
      }
      listener(netInterface, address, port, data);
    };
    this.#socket.on("message", messageListener);
    return {
      close: async () => {
        this.#socket.removeListener("message", messageListener);
      }
    };
  }
  /**
   * At minimum once every second we check for dangling sends that are not completed. That means that a dangling send
   * is removed very latest after <2s.
   */
  #rejectDanglingSends() {
    if (this.#sendsInProgress.size === 0) {
      return;
    }
    const now = import_general.Time.nowMs;
    for (const [promise, { sendMs, rejecter }] of this.#sendsInProgress) {
      const elapsed = (0, import_general.Millis)(now - sendMs);
      if (elapsed >= UDP_SEND_TIMEOUT_CHECK_INTERVAL) {
        this.#sendsInProgress.delete(promise);
        rejecter(new import_general.NetworkError("UDP send timeout"));
      }
    }
    if (this.#sendsInProgress.size > 0) {
      this.#sendTimer.start();
    }
  }
  async send(host, port, data) {
    const { promise, resolver, rejecter } = (0, import_general.createPromise)();
    const rejectOrResolve = (error) => {
      if (!this.#sendsInProgress.has(promise)) {
        return;
      }
      this.#sendsInProgress.delete(promise);
      if (!error) {
        resolver();
      } else {
        let repackAs;
        if ("code" in error) {
          switch (error.code) {
            case "EHOSTUNREACH":
              repackAs = import_general.AddressUnreachableError;
              break;
            case "ENETUNREACH":
              repackAs = import_general.NetworkUnreachableError;
              break;
            default:
              repackAs = import_general.NetworkError;
              break;
          }
        } else {
          repackAs = import_general.NetworkError;
        }
        rejecter((0, import_general.repackErrorAs)(error, repackAs));
      }
    };
    let sendHost = host;
    if (host.startsWith("ff") && !host.includes("%")) {
      const effectiveInterface = this.#netInterface ?? this.#observedInterface;
      if (effectiveInterface) {
        sendHost = `${host}%${effectiveInterface}`;
      } else {
        throw new import_general.NetworkError(
          `Cannot send to multicast address ${host} without a known network interface. Configure network.listeningAddressIpv6 to bind to a specific interface.`
        );
      }
    }
    this.#sendsInProgress.set(promise, { sendMs: import_general.Time.nowMs, rejecter });
    if (!this.#sendTimer.isRunning) {
      this.#sendTimer.start();
    }
    try {
      this.#socket.send(import_general.Bytes.of(data), port, sendHost, (error) => rejectOrResolve(error));
    } catch (error) {
      rejectOrResolve((0, import_general.repackErrorAs)(error, import_general.NetworkError));
    }
    return promise;
  }
  async close() {
    var _stack = [];
    try {
      const _closing = __using(_stack, this.#lifetime.closing());
      this.#sendTimer.stop();
      try {
        await new Promise((resolve) => this.#socket.close(resolve));
      } catch (error) {
        if (!(error instanceof Error) || error.message !== "Not running") {
          logger.debug("Error on closing socket", error);
        }
      }
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      __callDispose(_stack, _error, _hasError);
    }
  }
  get port() {
    return this.#socket.address().port;
  }
  supports(type, address) {
    if (type !== import_general.ChannelType.UDP) {
      return false;
    }
    if (address === void 0) {
      return true;
    }
    if (this.#type === "udp4") {
      return (0, import_general.isIPv4)(address);
    }
    return (0, import_general.isIPv6)(address);
  }
}
//# sourceMappingURL=NodeJsUdpChannel.js.map
