"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;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Group = void 0;
const node_assert_1 = __importDefault(require("node:assert"));
const logger_1 = require("../../utils/logger");
const Zcl = __importStar(require("../../zspec/zcl"));
const zclTransactionSequenceNumber_1 = __importDefault(require("../helpers/zclTransactionSequenceNumber"));
const device_1 = __importDefault(require("./device"));
const entity_1 = __importDefault(require("./entity"));
const NS = 'zh:controller:group';
class Group extends entity_1.default {
    databaseID;
    groupID;
    _members;
    // Can be used by applications to store data.
    meta;
    // This lookup contains all groups that are queried from the database, this is to ensure that always
    // the same instance is returned.
    static groups = new Map();
    static loadedFromDatabase = false;
    /** Member endpoints with valid devices (not unknown/deleted) */
    get members() {
        return this._members.filter((e) => e.getDevice() !== undefined);
    }
    constructor(databaseID, groupID, members, meta) {
        super();
        this.databaseID = databaseID;
        this.groupID = groupID;
        this._members = members;
        this.meta = meta;
    }
    /*
     * CRUD
     */
    /**
     * Reset runtime lookups.
     */
    static resetCache() {
        Group.groups.clear();
        Group.loadedFromDatabase = false;
    }
    static fromDatabaseEntry(entry) {
        // db is expected to never contain duplicate, so no need for explicit check
        const members = [];
        for (const member of entry.members) {
            const device = device_1.default.byIeeeAddr(member.deviceIeeeAddr);
            if (device) {
                const endpoint = device.getEndpoint(member.endpointID);
                if (endpoint) {
                    members.push(endpoint);
                }
            }
        }
        return new Group(entry.id, entry.groupID, members, entry.meta);
    }
    toDatabaseRecord() {
        const members = [];
        for (const member of this._members) {
            const device = member.getDevice();
            if (device) {
                members.push({ deviceIeeeAddr: device.ieeeAddr, endpointID: member.ID });
            }
        }
        return { id: this.databaseID, type: 'Group', groupID: this.groupID, members, meta: this.meta };
    }
    static loadFromDatabaseIfNecessary() {
        if (!Group.loadedFromDatabase) {
            for (const entry of entity_1.default.database.getEntriesIterator(['Group'])) {
                const group = Group.fromDatabaseEntry(entry);
                Group.groups.set(group.groupID, group);
            }
            Group.loadedFromDatabase = true;
        }
    }
    static byGroupID(groupID) {
        Group.loadFromDatabaseIfNecessary();
        return Group.groups.get(groupID);
    }
    /**
     * @deprecated use allIterator()
     */
    static all() {
        Group.loadFromDatabaseIfNecessary();
        return Array.from(Group.groups.values());
    }
    static *allIterator(predicate) {
        Group.loadFromDatabaseIfNecessary();
        for (const group of Group.groups.values()) {
            if (!predicate || predicate(group)) {
                yield group;
            }
        }
    }
    static create(groupID) {
        (0, node_assert_1.default)(typeof groupID === 'number', 'GroupID must be a number');
        // Don't allow groupID 0, from the spec:
        // "Scene identifier 0x00, along with group identifier 0x0000, is reserved for the global scene used by the OnOff cluster"
        (0, node_assert_1.default)(groupID >= 1, 'GroupID must be at least 1');
        Group.loadFromDatabaseIfNecessary();
        if (Group.groups.has(groupID)) {
            throw new Error(`Group with groupID '${groupID}' already exists`);
        }
        const databaseID = entity_1.default.database.newID();
        const group = new Group(databaseID, groupID, [], {});
        entity_1.default.database.insert(group.toDatabaseRecord());
        Group.groups.set(group.groupID, group);
        return group;
    }
    async removeFromNetwork() {
        for (const endpoint of this._members) {
            await endpoint.removeFromGroup(this);
        }
        this.removeFromDatabase();
    }
    removeFromDatabase() {
        Group.loadFromDatabaseIfNecessary();
        if (entity_1.default.database.has(this.databaseID)) {
            entity_1.default.database.remove(this.databaseID);
        }
        Group.groups.delete(this.groupID);
    }
    save(writeDatabase = true) {
        entity_1.default.database.update(this.toDatabaseRecord(), writeDatabase);
    }
    addMember(endpoint) {
        if (!this._members.includes(endpoint)) {
            this._members.push(endpoint);
            this.save();
        }
    }
    removeMember(endpoint) {
        const i = this._members.indexOf(endpoint);
        if (i > -1) {
            this._members.splice(i, 1);
            this.save();
        }
    }
    hasMember(endpoint) {
        return this._members.includes(endpoint);
    }
    /*
     * Zigbee functions
     */
    async write(clusterKey, attributes, options) {
        const optionsWithDefaults = this.getOptionsWithDefaults(options, Zcl.Direction.CLIENT_TO_SERVER);
        const cluster = Zcl.Utils.getCluster(clusterKey, undefined, {});
        const payload = [];
        for (const [nameOrID, value] of Object.entries(attributes)) {
            if (cluster.hasAttribute(nameOrID)) {
                const attribute = cluster.getAttribute(nameOrID);
                payload.push({ attrId: attribute.ID, attrData: value, dataType: attribute.type });
            }
            else if (!isNaN(Number(nameOrID))) {
                payload.push({ attrId: Number(nameOrID), attrData: value.value, dataType: value.type });
            }
            else {
                throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`);
            }
        }
        const createLogMessage = () => `Write ${this.groupID} ${cluster.name}(${JSON.stringify(attributes)}, ${JSON.stringify(optionsWithDefaults)})`;
        logger_1.logger.debug(createLogMessage, NS);
        try {
            const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, optionsWithDefaults.direction, true, optionsWithDefaults.manufacturerCode, optionsWithDefaults.transactionSequenceNumber ?? zclTransactionSequenceNumber_1.default.next(), 'write', cluster.ID, payload, {}, optionsWithDefaults.reservedBits);
            await entity_1.default.adapter.sendZclFrameToGroup(this.groupID, frame, optionsWithDefaults.srcEndpoint);
        }
        catch (error) {
            const err = error;
            err.message = `${createLogMessage()} failed (${err.message})`;
            logger_1.logger.debug(err.stack, NS);
            throw error;
        }
    }
    async read(clusterKey, attributes, options) {
        const optionsWithDefaults = this.getOptionsWithDefaults(options, Zcl.Direction.CLIENT_TO_SERVER);
        const cluster = Zcl.Utils.getCluster(clusterKey, undefined, {});
        const payload = [];
        for (const attribute of attributes) {
            payload.push({ attrId: typeof attribute === 'number' ? attribute : cluster.getAttribute(attribute).ID });
        }
        const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, optionsWithDefaults.direction, true, optionsWithDefaults.manufacturerCode, optionsWithDefaults.transactionSequenceNumber ?? zclTransactionSequenceNumber_1.default.next(), 'read', cluster.ID, payload, {}, optionsWithDefaults.reservedBits);
        const createLogMessage = () => `Read ${this.groupID} ${cluster.name}(${JSON.stringify(attributes)}, ${JSON.stringify(optionsWithDefaults)})`;
        logger_1.logger.debug(createLogMessage, NS);
        try {
            await entity_1.default.adapter.sendZclFrameToGroup(this.groupID, frame, optionsWithDefaults.srcEndpoint);
        }
        catch (error) {
            const err = error;
            err.message = `${createLogMessage()} failed (${err.message})`;
            logger_1.logger.debug(err.stack, NS);
            throw error;
        }
    }
    async command(clusterKey, commandKey, payload, options) {
        const optionsWithDefaults = this.getOptionsWithDefaults(options, Zcl.Direction.CLIENT_TO_SERVER);
        const cluster = Zcl.Utils.getCluster(clusterKey, undefined, {});
        const command = cluster.getCommand(commandKey);
        const createLogMessage = () => `Command ${this.groupID} ${cluster.name}.${command.name}(${JSON.stringify(payload)})`;
        logger_1.logger.debug(createLogMessage, NS);
        try {
            const frame = Zcl.Frame.create(Zcl.FrameType.SPECIFIC, optionsWithDefaults.direction, true, optionsWithDefaults.manufacturerCode, optionsWithDefaults.transactionSequenceNumber || zclTransactionSequenceNumber_1.default.next(), command.ID, cluster.ID, payload, {}, optionsWithDefaults.reservedBits);
            await entity_1.default.adapter.sendZclFrameToGroup(this.groupID, frame, optionsWithDefaults.srcEndpoint);
        }
        catch (error) {
            const err = error;
            err.message = `${createLogMessage()} failed (${err.message})`;
            logger_1.logger.debug(err.stack, NS);
            throw error;
        }
    }
    getOptionsWithDefaults(options, direction) {
        return {
            direction,
            srcEndpoint: undefined,
            reservedBits: 0,
            manufacturerCode: undefined,
            transactionSequenceNumber: undefined,
            ...(options || {}),
        };
    }
}
exports.Group = Group;
exports.default = Group;
//# sourceMappingURL=group.js.map