import { CCAPI, PhysicalCCAPI, getAPI, normalizeCCNameOrId, } from "@zwave-js/cc";
import { ZWaveError, ZWaveErrorCodes, getCCName, securityClassIsS2, } from "@zwave-js/core";
import { staticExtends } from "@zwave-js/shared";
import { distinct } from "alcalzone-shared/arrays";
import { createMultiCCAPIWrapper } from "./MultiCCAPIWrapper.js";
import { VirtualNode } from "./VirtualNode.js";
/**
 * Represents an endpoint of a virtual (broadcast, multicast) Z-Wave node.
 * This can either be the root device itself (index 0) or a more specific endpoint like a single plug.
 *
 * The endpoint's capabilities are determined by the capabilities of the individual nodes' endpoints.
 */
export class VirtualEndpoint {
    driver;
    index;
    constructor(
    /** The virtual node this endpoint belongs to */
    node, 
    /** The driver instance this endpoint belongs to */
    driver, 
    /** The index of this endpoint. 0 for the root device, 1+ otherwise */
    index) {
        this.driver = driver;
        this.index = index;
        if (node)
            this._node = node;
    }
    /** Required by {@link IZWaveEndpoint} */
    virtual = true;
    /** The virtual node this endpoint belongs to */
    _node;
    get node() {
        return this._node;
    }
    setNode(node) {
        this._node = node;
    }
    get nodeId() {
        // Use the defined node ID if it exists
        if (this.node.id != undefined)
            return this.node.id;
        // Otherwise deduce it from the physical nodes
        const ret = this.node.physicalNodes.map((n) => n.id);
        if (ret.length === 1)
            return ret[0];
        return ret;
    }
    /** Tests if this endpoint supports the given CommandClass */
    supportsCC(cc) {
        // A virtual endpoints supports a CC if any of the physical endpoints it targets supports the CC non-securely
        return this.node.physicalNodes.some((n) => {
            const endpoint = n.getEndpoint(this.index);
            return endpoint?.supportsCC(cc);
        });
    }
    /**
     * Retrieves the minimum non-zero version of the given CommandClass the physical endpoints implement
     * Returns 0 if the CC is not supported at all.
     */
    getCCVersion(cc) {
        const nonZeroVersions = this.node.physicalNodes
            .map((n) => n.getEndpoint(this.index)?.getCCVersion(cc))
            .filter((v) => v != undefined && v > 0);
        if (!nonZeroVersions.length)
            return 0;
        return Math.min(...nonZeroVersions);
    }
    /**
     * @internal
     * Creates an API instance for a given command class. Throws if no API is defined.
     * @param ccId The command class to create an API instance for
     */
    createAPI(ccId) {
        const createCCAPI = (endpoint, secClass) => {
            if (securityClassIsS2(secClass)
                // No need to do multicast if there is only one node
                && endpoint.node.physicalNodes.length > 1) {
                // The API for S2 needs to know the multicast group ID
                const secMan = this.driver.getSecurityManager2(endpoint.node.physicalNodes[0].id);
                return CCAPI.create(ccId, this.driver, endpoint).withOptions({
                    s2MulticastGroupId: secMan?.createMulticastGroup(endpoint.node.physicalNodes.map((n) => n.id), secClass),
                });
            }
            else {
                return CCAPI.create(ccId, this.driver, endpoint);
            }
        };
        // For mixed security classes, we need to create a wrapper
        // that handles calling multiple API instances
        if (this.node.hasMixedSecurityClasses) {
            const apiInstances = [
                ...this.node.nodesBySecurityClass.entries(),
            ].map(([secClass, nodes]) => {
                // We need a separate virtual endpoint for each security class, so the API instances
                // access the correct nodes.
                const node = new VirtualNode(this.node.id, this.driver, nodes);
                const endpoint = node.getEndpoint(this.index) ?? node;
                return createCCAPI(endpoint, secClass);
            });
            return createMultiCCAPIWrapper(apiInstances);
        }
        else {
            // The node has a single security class, just reuse it
            const securityClass = [...this.node.nodesBySecurityClass.keys()][0];
            return createCCAPI(this, securityClass);
        }
    }
    _commandClassAPIs = new Map();
    _commandClassAPIsProxy = new Proxy(this._commandClassAPIs, {
        get: (target, ccNameOrId) => {
            // Avoid ultra-weird error messages during testing
            if (process.env.NODE_ENV === "test"
                && typeof ccNameOrId === "string"
                && (ccNameOrId === "$$typeof"
                    || ccNameOrId === "constructor"
                    || ccNameOrId.includes("@@__IMMUTABLE"))) {
                return undefined;
            }
            if (typeof ccNameOrId === "symbol") {
                // Allow access to the iterator symbol
                if (ccNameOrId === Symbol.iterator) {
                    return this.commandClassesIterator;
                }
                else if (ccNameOrId === Symbol.toStringTag) {
                    return "[object Object]";
                }
                // ignore all other symbols
                return undefined;
            }
            else {
                // The command classes are exposed to library users by their name or the ID
                const ccId = normalizeCCNameOrId(ccNameOrId);
                if (ccId == undefined) {
                    throw new ZWaveError(`Command Class ${ccNameOrId} is not implemented!`, ZWaveErrorCodes.CC_NotImplemented);
                }
                // When accessing a CC API for the first time, we need to create it
                if (!target.has(ccId)) {
                    const api = this.createAPI(ccId);
                    target.set(ccId, api);
                }
                return target.get(ccId);
            }
        },
    });
    /**
     * Used to iterate over the commandClasses API without throwing errors by accessing unsupported CCs
     */
    commandClassesIterator = function* () {
        const allCCs = distinct(this._node.physicalNodes
            .map((n) => n.getEndpoint(this.index))
            .filter((e) => !!e)
            .flatMap((e) => [...e.implementedCommandClasses.keys()]));
        for (const cc of allCCs) {
            if (this.supportsCC(cc)) {
                // When a CC is supported, it can still happen that the CC API
                // cannot be created for virtual endpoints
                const APIConstructor = getAPI(cc);
                if (staticExtends(APIConstructor, PhysicalCCAPI))
                    continue;
                yield this.commandClasses[cc];
            }
        }
    }.bind(this);
    /**
     * Provides access to simplified APIs that are tailored to specific CCs.
     * Make sure to check support of each API using `API.isSupported()` since
     * all other API calls will throw if the API is not supported
     */
    get commandClasses() {
        return this._commandClassAPIsProxy;
    }
    /** Allows checking whether a CC API is supported before calling it with {@link VirtualEndpoint.invokeCCAPI} */
    supportsCCAPI(cc) {
        // No need to validate the `cc` parameter, the following line will throw for invalid CCs
        return this.commandClasses[cc].isSupported();
    }
    /**
     * Allows dynamically calling any CC API method on this virtual endpoint by CC ID and method name.
     * Use {@link VirtualEndpoint.supportsCCAPI} to check support first.
     *
     * **Warning:** Get-type commands are not supported, even if auto-completion indicates that they are.
     */
    invokeCCAPI(cc, method, ...args) {
        // No need to validate the `cc` parameter, the following line will throw for invalid CCs
        const CCAPI = this.commandClasses[cc];
        const ccId = normalizeCCNameOrId(cc);
        const ccName = getCCName(ccId);
        if (!CCAPI) {
            throw new ZWaveError(`The API for the ${ccName} CC does not exist or is not implemented!`, ZWaveErrorCodes.CC_NoAPI);
        }
        const apiMethod = CCAPI[method];
        if (typeof apiMethod !== "function") {
            throw new ZWaveError(`Method "${method}" does not exist on the API for the ${ccName} CC!`, ZWaveErrorCodes.CC_NotImplemented);
        }
        return apiMethod.apply(CCAPI, args);
    }
}
//# sourceMappingURL=VirtualEndpoint.js.map