import { BasicCCValues, SetValueStatus, supervisionResultToSetValueResult, } from "@zwave-js/cc";
import { SecurityClass, SupervisionStatus, ZWaveError, ZWaveErrorCodes, actuatorCCs, getCCName, isSupervisionResult, isZWaveError, normalizeValueID, supervisedCommandSucceeded, valueIdToString, } from "@zwave-js/core";
import { distinct } from "alcalzone-shared/arrays";
import { VirtualEndpoint } from "./VirtualEndpoint.js";
function groupNodesBySecurityClass(nodes) {
    const ret = new Map();
    for (const node of nodes) {
        const secClass = node.getHighestSecurityClass();
        if (secClass === SecurityClass.Temporary || secClass == undefined) {
            continue;
        }
        if (!ret.has(secClass)) {
            ret.set(secClass, []);
        }
        ret.get(secClass).push(node);
    }
    return ret;
}
export class VirtualNode extends VirtualEndpoint {
    id;
    constructor(id, driver, 
    /** The references to the physical node this virtual node abstracts */
    physicalNodes) {
        // Define this node's intrinsic endpoint as the root device (0)
        super(undefined, driver, 0);
        this.id = id;
        // Set the reference to this and the physical nodes
        super.setNode(this);
        this.physicalNodes = [...physicalNodes].filter((n) => 
        // And avoid including the controller node in the support checks
        n.id !== driver.controller.ownNodeId
            // And omit nodes using Security S0 which does not support broadcast / multicast
            && n.getHighestSecurityClass() !== SecurityClass.S0_Legacy);
        this.nodesBySecurityClass = groupNodesBySecurityClass(this.physicalNodes);
        // If broadcasting is attempted with mixed security classes, automatically fall back to multicast
        if (this.hasMixedSecurityClasses)
            this.id = undefined;
    }
    physicalNodes;
    nodesBySecurityClass;
    get hasMixedSecurityClasses() {
        return this.nodesBySecurityClass.size > 1;
    }
    /**
     * Updates a value for a given property of a given CommandClass.
     * This will communicate with the physical node(s) this virtual node represents!
     */
    async setValue(valueId, value, options) {
        // Ensure we're dealing with a valid value ID, with no extra properties
        valueId = normalizeValueID(valueId);
        // Try to retrieve the corresponding CC API
        try {
            // Access the CC API by name
            const endpointInstance = this.getEndpoint(valueId.endpoint || 0);
            if (!endpointInstance) {
                return {
                    status: SetValueStatus.EndpointNotFound,
                    message: `Endpoint ${valueId.endpoint} does not exist on virtual node ${this.id ?? "??"}`,
                };
            }
            let api = endpointInstance.commandClasses[valueId.commandClass];
            // Check if the setValue method is implemented
            if (!api.setValue) {
                return {
                    status: SetValueStatus.NotImplemented,
                    message: `The ${getCCName(valueId.commandClass)} CC does not support setting values`,
                };
            }
            const valueIdProps = {
                property: valueId.property,
                propertyKey: valueId.propertyKey,
            };
            const hooks = api.setValueHooks?.(valueIdProps, value, options);
            if (hooks?.supervisionDelayedUpdates) {
                api = api.withOptions({
                    requestStatusUpdates: true,
                    onUpdate: async (update) => {
                        try {
                            if (update.status === SupervisionStatus.Success) {
                                await hooks.supervisionOnSuccess();
                            }
                            else if (update.status === SupervisionStatus.Fail) {
                                await hooks.supervisionOnFailure();
                            }
                        }
                        catch {
                            // TODO: Log error?
                        }
                    },
                });
            }
            // If the caller wants progress updates, they shall have them
            if (typeof options?.onProgress === "function") {
                api = api.withOptions({
                    onProgress: options.onProgress,
                });
            }
            // And call it
            const result = await api.setValue.call(api, valueIdProps, value, options);
            if (api.isSetValueOptimistic(valueId)) {
                // If the call did not throw, assume that the call was successful and remember the new value
                // for each node that was affected by this command
                const affectedNodes = this.physicalNodes
                    .filter((node) => node
                    .getEndpoint(endpointInstance.index)
                    ?.supportsCC(valueId.commandClass));
                for (const node of affectedNodes) {
                    node.valueDB.setValue(valueId, value);
                }
            }
            // Depending on the settings of the SET_VALUE implementation, we may have to
            // optimistically update a different value and/or verify the changes
            if (hooks) {
                const supervisedAndSuccessful = isSupervisionResult(result)
                    && result.status === SupervisionStatus.Success;
                const shouldUpdateOptimistically = api.isSetValueOptimistic(valueId)
                    // For successful supervised commands, we know that an optimistic update is ok
                    && (supervisedAndSuccessful
                        // For unsupervised commands that did not fail, we let the applciation decide whether
                        // to update related value optimistically
                        || (!this.driver.options.disableOptimisticValueUpdate
                            && result == undefined));
                // The actual API implementation handles additional optimistic updates
                if (shouldUpdateOptimistically) {
                    hooks.optimisticallyUpdateRelatedValues?.(supervisedAndSuccessful);
                }
                // Verify the current value after a delay, unless...
                // ...the command was supervised and successful
                // ...and the CC API decides not to verify anyways
                if (!supervisedCommandSucceeded(result)
                    || hooks.forceVerifyChanges?.()) {
                    // Let the CC API implementation handle the verification.
                    // It may still decide not to do it.
                    await hooks.verifyChanges?.(result);
                }
            }
            return supervisionResultToSetValueResult(result);
        }
        catch (e) {
            // Define which errors during setValue are expected and won't throw an error
            if (isZWaveError(e)) {
                let result;
                switch (e.code) {
                    // This CC or API is not implemented
                    case ZWaveErrorCodes.CC_NotImplemented:
                    case ZWaveErrorCodes.CC_NoAPI:
                        result = {
                            status: SetValueStatus.NotImplemented,
                            message: e.message,
                        };
                        break;
                    // A user tried to set an invalid value
                    case ZWaveErrorCodes.Argument_Invalid:
                        result = {
                            status: SetValueStatus.InvalidValue,
                            message: e.message,
                        };
                        break;
                }
                if (result)
                    return result;
            }
            throw e;
        }
    }
    /**
     * Returns a list of all value IDs and their metadata that can be used to
     * control the physical node(s) this virtual node represents.
     */
    getDefinedValueIDs() {
        // In order to compare value ids, we need them to be strings
        const ret = new Map();
        for (const pNode of this.physicalNodes) {
            // // Nodes using Security S0 cannot be used for broadcast
            // if (pNode.getHighestSecurityClass() === SecurityClass.S0_Legacy) {
            // 	continue;
            // }
            // Take only the actuator values
            const valueIDs = pNode
                .getDefinedValueIDs()
                .filter((v) => actuatorCCs.includes(v.commandClass));
            // And add them to the returned array if they aren't included yet or if the version is higher
            for (const valueId of valueIDs) {
                const mapKey = valueIdToString(valueId);
                const ccVersion = pNode.getCCVersion(valueId.commandClass);
                const metadata = pNode.getValueMetadata(valueId);
                // Don't expose read-only values for virtual nodes, they won't ever have any value
                if (!metadata.writeable)
                    continue;
                const needsUpdate = !ret.has(mapKey)
                    || ret.get(mapKey).ccVersion < ccVersion;
                if (needsUpdate) {
                    ret.set(mapKey, {
                        ...valueId,
                        ccVersion,
                        metadata: {
                            ...metadata,
                            // Metadata of virtual nodes is only writable
                            readable: false,
                        },
                    });
                }
            }
        }
        // Basic CC is not exposed, but virtual nodes need it to control multiple different devices together
        const exposedEndpoints = distinct([...ret.values()]
            .map((v) => v.endpoint)
            .filter((e) => e !== undefined));
        for (const endpoint of exposedEndpoints) {
            // TODO: This should be defined in the Basic CC file
            const valueId = {
                ...BasicCCValues.targetValue.endpoint(endpoint),
                commandClassName: "Basic",
                propertyName: "Target value",
            };
            const ccVersion = 1;
            const metadata = {
                ...BasicCCValues.targetValue.meta,
                readable: false,
            };
            ret.set(valueIdToString(valueId), {
                ...valueId,
                ccVersion,
                metadata,
            });
        }
        return [...ret.values()];
    }
    /** Cache for this node's endpoint instances */
    _endpointInstances = new Map();
    getEndpoint(index) {
        if (index < 0) {
            throw new ZWaveError("The endpoint index must be positive!", ZWaveErrorCodes.Argument_Invalid);
        }
        // Zero is the root endpoint - i.e. this node. Also accept undefined if an application misbehaves
        if (!index)
            return this;
        // Check if the Multi Channel CC interviews for all nodes are completed,
        // because we don't have all the information before that
        if (!this.isMultiChannelInterviewComplete) {
            this.driver.driverLog.print(`Virtual node ${this.id ?? "??"}, Endpoint ${index}: Trying to access endpoint instance before the Multi Channel interview of all nodes was completed!`, "error");
            return undefined;
        }
        // Check if the requested endpoint exists on any physical node
        if (index > this.getEndpointCount())
            return undefined;
        // Create an endpoint instance if it does not exist
        if (!this._endpointInstances.has(index)) {
            this._endpointInstances.set(index, new VirtualEndpoint(this, this.driver, index));
        }
        return this._endpointInstances.get(index);
    }
    getEndpointOrThrow(index) {
        const ret = this.getEndpoint(index);
        if (!ret) {
            throw new ZWaveError(`Endpoint ${index} does not exist on virtual node ${this.id ?? "??"}`, ZWaveErrorCodes.Controller_EndpointNotFound);
        }
        return ret;
    }
    /** Returns the current endpoint count of this virtual node (the maximum in the list of physical nodes) */
    getEndpointCount() {
        let ret = 0;
        for (const node of this.physicalNodes) {
            const count = node.getEndpointCount();
            ret = Math.max(ret, count);
        }
        return ret;
    }
    get isMultiChannelInterviewComplete() {
        for (const node of this.physicalNodes) {
            if (!node["isMultiChannelInterviewComplete"])
                return false;
        }
        return true;
    }
}
//# sourceMappingURL=VirtualNode.js.map