"use strict";
/* v8 ignore start */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EmberOneWaitress = exports.OneWaitressEvents = void 0;
const es6_1 = __importDefault(require("fast-deep-equal/es6"));
const consts_1 = require("../../../zspec/consts");
const zcl_1 = require("../../../zspec/zcl");
/** Events specific to OneWaitress usage. */
var OneWaitressEvents;
(function (OneWaitressEvents) {
    OneWaitressEvents["STACK_STATUS_NETWORK_UP"] = "STACK_STATUS_NETWORK_UP";
    OneWaitressEvents["STACK_STATUS_NETWORK_DOWN"] = "STACK_STATUS_NETWORK_DOWN";
    OneWaitressEvents["STACK_STATUS_NETWORK_OPENED"] = "STACK_STATUS_NETWORK_OPENED";
    OneWaitressEvents["STACK_STATUS_NETWORK_CLOSED"] = "STACK_STATUS_NETWORK_CLOSED";
})(OneWaitressEvents || (exports.OneWaitressEvents = OneWaitressEvents = {}));
/**
 * The one waitress to rule them all. Hopefully.
 * Careful, she'll burn you if you're late on delivery!
 *
 * NOTE: `messageTag` is unreliable, so not used...
 */
class EmberOneWaitress {
    // biome-ignore lint/suspicious/noExplicitAny: API
    waiters;
    // NOTE: for now, this could be much simpler (array-like), but more complex events might come into play
    // biome-ignore lint/suspicious/noExplicitAny: API
    eventWaiters;
    currentId;
    currentEventId;
    constructor() {
        this.waiters = new Map();
        this.eventWaiters = new Map();
        this.currentId = 0;
        this.currentEventId = 0;
    }
    /**
     * Reject because of failed delivery notified by `ezspMessageSentHandler`.
     * NOTE: This checks for APS sequence, which is only valid in `ezspMessageSentHandler`, not `ezspIncomingMessageHandler` (sequence from stack)
     *
     * @param target
     * @param apsFrame
     * @returns
     */
    deliveryFailedFor(target, apsFrame) {
        for (const [index, waiter] of this.waiters.entries()) {
            if (waiter.timedout) {
                this.waiters.delete(index);
                continue;
            }
            // no target in touchlink
            // in `ezspMessageSentHandler`, the clusterId for ZDO is still the request one, so check against apsFrame, not override
            if ((waiter.matcher.apsFrame.profileId === consts_1.TOUCHLINK_PROFILE_ID || target === waiter.matcher.target) &&
                apsFrame.sequence === waiter.matcher.apsFrame.sequence &&
                apsFrame.profileId === waiter.matcher.apsFrame.profileId &&
                apsFrame.clusterId === waiter.matcher.apsFrame.clusterId) {
                clearTimeout(waiter.timer);
                waiter.resolved = true;
                this.waiters.delete(index);
                waiter.reject(new Error(`Delivery failed for '${target}'.`));
                return true;
            }
        }
        return false;
    }
    /**
     * Resolve ZDO response payload.
     * @param sender Node ID or EUI64 in the response
     * @param apsFrame APS Frame in the response
     * @param payload Payload to resolve
     * @returns True if resolved a waiter
     */
    resolveZDO(sender, apsFrame, payload) {
        for (const [index, waiter] of this.waiters.entries()) {
            if (waiter.timedout) {
                this.waiters.delete(index);
                continue;
            }
            if (waiter.matcher.zdoResponseClusterId !== undefined && // skip if not a zdo waiter
                sender === waiter.matcher.target && // always a sender expected in ZDO
                apsFrame.profileId === waiter.matcher.apsFrame.profileId && // profileId is a bit redundant here, but...
                apsFrame.clusterId === waiter.matcher.zdoResponseClusterId) {
                clearTimeout(waiter.timer);
                waiter.resolved = true;
                this.waiters.delete(index);
                waiter.resolve(payload);
                return true;
            }
        }
        return false;
    }
    /**
     * Resolve ZCL response payload
     * @param payload Payload to resolve
     * @returns True if resolved a waiter
     */
    resolveZCL(payload) {
        if (!payload.header)
            return false;
        for (const [index, waiter] of this.waiters.entries()) {
            if (waiter.timedout) {
                this.waiters.delete(index);
                continue;
            }
            const { matcher } = waiter;
            const { address, endpoint, clusterID, header } = payload;
            // no target in touchlink, also no APS sequence, but use the ZCL one instead
            if ((matcher.apsFrame.profileId === consts_1.TOUCHLINK_PROFILE_ID ||
                (address === matcher.target && endpoint === matcher.apsFrame.destinationEndpoint)) &&
                clusterID === matcher.apsFrame.clusterId &&
                (matcher.zclSequence === undefined || header.transactionSequenceNumber === matcher.zclSequence) &&
                (matcher.commandIdentifier === undefined ||
                    header.commandIdentifier === matcher.commandIdentifier ||
                    // defaultRsp
                    (header.frameControl.frameType === zcl_1.FrameType.GLOBAL &&
                        header.commandIdentifier === 0x0b &&
                        (matcher.defaultRspCommandId === undefined || payload.data[payload.data.byteLength - 2] === matcher.defaultRspCommandId)))) {
                clearTimeout(waiter.timer);
                waiter.resolved = true;
                this.waiters.delete(index);
                waiter.resolve(payload);
                return true;
            }
        }
        return false;
    }
    waitFor(matcher, timeout) {
        this.currentId += 1;
        const id = this.currentId;
        this.currentId &= 0xffff; // roll-over every so often - 65535 should be enough not to create conflicts ;-)
        const promise = new Promise((resolve, reject) => {
            const object = { matcher, resolve, reject, timedout: false, resolved: false, id };
            this.waiters.set(id, object);
        });
        const start = () => {
            const waiter = this.waiters.get(id);
            if (waiter && !waiter.resolved && !waiter.timer) {
                // Capture the stack trace from the caller of start()
                const error = new Error();
                Error.captureStackTrace(error);
                waiter.timer = setTimeout(() => {
                    error.message = `${JSON.stringify(matcher)} timed out after ${timeout}ms`;
                    waiter.timedout = true;
                    waiter.reject(error);
                }, timeout);
            }
            return { promise, id };
        };
        return { id, start };
    }
    /**
     * Shortcut that starts the timer immediately and returns the promise.
     * No access to `id`, so no easy cancel.
     * @param matcher
     * @param timeout
     * @returns
     */
    startWaitingFor(matcher, timeout) {
        return this.waitFor(matcher, timeout).start().promise;
    }
    remove(id) {
        const waiter = this.waiters.get(id);
        if (waiter) {
            if (!waiter.timedout && waiter.timer) {
                clearTimeout(waiter.timer);
            }
            this.waiters.delete(id);
        }
    }
    /**
     * Matches event name with matcher's, and payload (if any in matcher) using `fast-deep-equal/es6` (all keys & values must match)
     * @param eventName
     * @param payload
     * @returns
     */
    resolveEvent(eventName, payload) {
        for (const [index, waiter] of this.eventWaiters.entries()) {
            if (waiter.timedout) {
                this.eventWaiters.delete(index);
                continue;
            }
            if (eventName === waiter.matcher.eventName && (!waiter.matcher.payload || (0, es6_1.default)(payload, waiter.matcher.payload))) {
                clearTimeout(waiter.timer);
                waiter.resolved = true;
                this.eventWaiters.delete(index);
                waiter.resolve(payload);
                return true;
            }
        }
        return false;
    }
    waitForEvent(matcher, timeout, reason) {
        // NOTE: logic is very much the same as `waitFor`, just different matcher
        this.currentEventId += 1;
        const id = this.currentEventId;
        this.currentEventId &= 0xffff; // roll-over every so often - 65535 should be enough not to create conflicts ;-)
        const promise = new Promise((resolve, reject) => {
            const object = { matcher, resolve, reject, timedout: false, resolved: false, id };
            this.eventWaiters.set(id, object);
        });
        const start = () => {
            const waiter = this.eventWaiters.get(id);
            if (waiter && !waiter.resolved && !waiter.timer) {
                // Capture the stack trace from the caller of start()
                const error = new Error();
                Error.captureStackTrace(error);
                waiter.timer = setTimeout(() => {
                    error.message = `${reason ? reason : JSON.stringify(matcher)} timed out after ${timeout}ms`;
                    waiter.timedout = true;
                    waiter.reject(error);
                }, timeout);
            }
            return { promise, id };
        };
        return { id, start };
    }
    /**
     * Shortcut that starts the timer immediately and returns the promise.
     * No access to `id`, so no easy cancel.
     * @param matcher
     * @param timeout
     * @param reason If supplied, will be used as timeout label, otherwise stringified matcher is.
     * @returns
     */
    startWaitingForEvent(matcher, timeout, reason) {
        return this.waitForEvent(matcher, timeout, reason).start().promise;
    }
    removeEvent(id) {
        const waiter = this.eventWaiters.get(id);
        if (waiter) {
            if (!waiter.timedout && waiter.timer) {
                clearTimeout(waiter.timer);
            }
            this.eventWaiters.delete(id);
        }
    }
}
exports.EmberOneWaitress = EmberOneWaitress;
//# sourceMappingURL=oneWaitress.js.map