import { getResponder as getMdnsResponder, } from "@homebridge/ciao";
import { BinarySwitchCCValues, MultilevelSwitchCCValues, NotificationCCValues, SoundSwitchCCValues, SwitchType, } from "@zwave-js/cc";
import { CommandClasses, } from "@zwave-js/core";
import { Faucet, ZWaveSerialFrameType, } from "@zwave-js/serial";
import { createAndOpenMockedZWaveSerialPort, } from "@zwave-js/serial/mock";
import { getErrorMessage } from "@zwave-js/shared";
import { MockController, MockNode, getDefaultMockEndpointCapabilities, getDefaultMockNodeCapabilities, } from "@zwave-js/testing";
import { createDeferredPromise } from "alcalzone-shared/deferred-promise";
import { createServer } from "node:net";
import { createDefaultMockControllerBehaviors, createDefaultMockNodeBehaviors, } from "./Testing.js";
import { ProtocolVersion } from "./Utils.js";
export class MockServer {
    options;
    constructor(options = {}) {
        this.options = options;
    }
    serialport;
    binding;
    server;
    responder;
    service;
    mockController;
    mockNodes;
    async start() {
        const { serial, port: mockPort } = await createAndOpenMockedZWaveSerialPort();
        this.serialport = serial;
        this.binding = mockPort;
        console.log("Mock serial port opened");
        // Hook up a fake controller and nodes
        ({ mockController: this.mockController, mockNodes: this.mockNodes } =
            prepareMocks(mockPort, serial, this.options.config?.controller, this.options.config?.nodes));
        // Call the init hook if it is defined
        if (typeof this.options.config?.onInit === "function") {
            this.options.config.onInit(this.mockController, this.mockNodes);
        }
        // Forward data from the serialport to the socket while one is connected
        const faucet = new Faucet(serial.readable);
        // Start a TCP server, listen for connections, and forward them to the serial port
        this.server = createServer((socket) => {
            if (!this.serialport) {
                console.error("Serial port not initialized");
                socket.destroy();
                return;
            }
            console.log("Client connected");
            // Wrap the socket in a writable stream
            const writable = new WritableStream({
                write: (chunk) => {
                    if (chunk.type !== ZWaveSerialFrameType.SerialAPI)
                        return;
                    if (typeof chunk.data === "number") {
                        socket.write(Uint8Array.from([chunk.data]));
                    }
                    else {
                        socket.write(chunk.data);
                    }
                },
            });
            // And forward data from the serial port
            faucet.connect(writable);
            socket.on("close", () => {
                faucet.disconnect();
                void writable.close();
                console.log("Client disconnected");
            });
            // Forward data from the socket to the serial port
            socket.on("data", async (chunk) => {
                await this.serialport?.writeAsync(chunk).catch((e) => {
                    console.error(`Error writing to serialport`, e);
                });
            });
        });
        const port = this.options.port ?? 5555;
        this.responder = getMdnsResponder();
        this.service = this.responder.createService({
            name: "zwave-mock-server",
            type: "zwave",
            protocol: "tcp" /* Protocol.TCP */,
            port,
            txt: {
                manufacturer: "Z-Wave JS",
                model: "Mock Server",
            },
        });
        // Do not allow more than one client to connect
        this.server.maxConnections = 1;
        const promise = createDeferredPromise();
        this.server.on("error", (err) => {
            if (err.code === "EADDRINUSE") {
                promise.reject(err);
            }
        });
        this.server.listen({
            host: this.options.interface,
            port,
        }, async () => {
            const address = this.server.address();
            console.log(`Server listening on tcp://${address.address}:${address.port}`);
            promise.resolve();
            // Advertise the service via mDNS
            try {
                await this.service.advertise();
                console.log(`Enabled mDNS service discovery.`);
            }
            catch (e) {
                console.error(`Failed to enable mDNS service discovery: ${getErrorMessage(e)}`);
            }
        });
    }
    async stop() {
        console.log("Shutting down mock server...");
        await this.service?.end();
        await this.service?.destroy();
        await this.responder?.shutdown();
        this.mockController?.destroy();
        this.server?.close();
        await this.serialport?.close();
        this.binding?.destroy();
        console.log("Mock server shut down");
    }
}
function prepareMocks(mockPort, serial, controller = {}, nodes = []) {
    const mockController = new MockController({
        homeId: 0x7e570001,
        ownNodeId: 1,
        ...controller,
        mockPort,
        serial,
    });
    // Apply default behaviors that are required for interacting with the driver correctly
    mockController.defineBehavior(...createDefaultMockControllerBehaviors());
    // Apply custom behaviors
    if (controller.behaviors) {
        mockController.defineBehavior(...controller.behaviors);
    }
    const mockNodes = [];
    for (const node of nodes) {
        const mockNode = new MockNode({
            ...node,
            controller: mockController,
        });
        mockController.addNode(mockNode);
        mockNodes.push(mockNode);
        // Apply default behaviors that are required for interacting with the driver correctly
        mockNode.defineBehavior(...createDefaultMockNodeBehaviors());
        // Apply custom behaviors
        if (node.behaviors) {
            mockNode.defineBehavior(...node.behaviors);
        }
    }
    return {
        mockController,
        mockNodes,
    };
}
export function createMockNodeOptionsFromDump(dump) {
    const ret = {
        id: dump.id,
    };
    ret.capabilities = getDefaultMockNodeCapabilities();
    if (typeof dump.isListening === "boolean") {
        ret.capabilities.isListening = dump.isListening;
    }
    if (dump.isFrequentListening !== "unknown") {
        ret.capabilities.isFrequentListening = dump.isFrequentListening;
    }
    if (typeof dump.isRouting === "boolean") {
        ret.capabilities.isRouting = dump.isRouting;
    }
    if (typeof dump.supportsBeaming === "boolean") {
        ret.capabilities.supportsBeaming = dump.supportsBeaming;
    }
    if (typeof dump.supportsSecurity === "boolean") {
        ret.capabilities.supportsSecurity = dump.supportsSecurity;
    }
    if (typeof dump.supportedDataRates === "boolean") {
        ret.capabilities.supportedDataRates = dump.supportedDataRates;
    }
    if (ProtocolVersion[dump.protocol] !== undefined) {
        ret.capabilities.protocolVersion =
            ProtocolVersion[dump.protocol];
    }
    if (dump.deviceClass !== "unknown") {
        ret.capabilities.basicDeviceClass = dump.deviceClass.basic.key;
        ret.capabilities.genericDeviceClass = dump.deviceClass.generic.key;
        ret.capabilities.specificDeviceClass = dump.deviceClass.specific.key;
    }
    ret.capabilities.firmwareVersion = dump.fingerprint.firmwareVersion;
    ret.capabilities.manufacturerId = parseInt(dump.fingerprint.manufacturerId, 16);
    ret.capabilities.productType = parseInt(dump.fingerprint.productType, 16);
    ret.capabilities.productId = parseInt(dump.fingerprint.productId, 16);
    for (const [ccName, ccDump] of Object.entries(dump.commandClasses)) {
        const ccId = CommandClasses[ccName];
        if (ccId == undefined)
            continue;
        // FIXME: Security encapsulation is not supported yet in mocks
        if (ccId === CommandClasses.Security
            || ccId === CommandClasses["Security 2"]) {
            continue;
        }
        // FIXME: Supervision encapsulation is not supported yet in mocks
        if (ccId === CommandClasses.Supervision) {
            continue;
        }
        // FIXME: Transport Service encapsulation is not supported yet in mocks
        if (ccId === CommandClasses["Transport Service"]) {
            continue;
        }
        ret.capabilities.commandClasses ??= [];
        ret.capabilities.commandClasses.push(createCCCapabilitiesFromDump(ccId, ccDump));
    }
    if (dump.endpoints) {
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        for (const [indexStr, endpointDump] of Object.entries(dump.endpoints)) {
            // FIXME: The mocks expect endpoints to be consecutive
            // const index = parseInt(indexStr);
            const epCaps = getDefaultMockEndpointCapabilities(
            // @ts-expect-error We are initializing the device classes above
            ret.capabilities);
            let epCCs;
            if (endpointDump.deviceClass !== "unknown") {
                epCaps.genericDeviceClass =
                    endpointDump.deviceClass.generic.key;
                epCaps.specificDeviceClass =
                    endpointDump.deviceClass.specific.key;
            }
            for (const [ccName, ccDump] of Object.entries(endpointDump.commandClasses)) {
                const ccId = CommandClasses[ccName];
                if (ccId == undefined)
                    continue;
                // FIXME: Security encapsulation is not supported yet in mocks
                if (ccId === CommandClasses.Security
                    || ccId === CommandClasses["Security 2"]) {
                    continue;
                }
                epCCs ??= [];
                epCCs.push(createCCCapabilitiesFromDump(ccId, ccDump));
            }
            ret.capabilities.endpoints ??= [];
            ret.capabilities.endpoints.push({
                ...epCaps,
                commandClasses: epCCs,
            });
        }
    }
    return ret;
}
function createCCCapabilitiesFromDump(ccId, dump) {
    const ret = {
        ccId,
        isSupported: dump.isSupported,
        isControlled: dump.isControlled,
        secure: dump.secure,
        version: dump.version,
    };
    // Parse CC specific info from values
    if (ccId === CommandClasses.Configuration) {
        Object.assign(ret, createConfigurationCCCapabilitiesFromDump(dump));
    }
    else if (ccId === CommandClasses.Notification) {
        Object.assign(ret, createNotificationCCCapabilitiesFromDump(dump));
    }
    else if (ccId === CommandClasses["Binary Switch"]) {
        Object.assign(ret, createBinarySwitchCCCapabilitiesFromDump(dump));
    }
    else if (ccId === CommandClasses["Multilevel Switch"]) {
        Object.assign(ret, createMultilevelSwitchCCCapabilitiesFromDump(dump));
    }
    else if (ccId === CommandClasses["Sound Switch"]) {
        Object.assign(ret, createSoundSwitchCCCapabilitiesFromDump(dump));
    }
    return ret;
}
function createConfigurationCCCapabilitiesFromDump(dump) {
    const ret = {
        bulkSupport: false,
        parameters: [],
    };
    for (const val of dump.values) {
        if (typeof val.property !== "number")
            continue;
        // Mocks don't support partial parameters
        if (val.propertyKey != undefined)
            continue;
        // Metadata contains the param information
        if (!val.metadata)
            continue;
        const meta = val.metadata;
        ret.parameters.push({
            "#": val.property,
            valueSize: meta.valueSize ?? 1,
            name: meta.label,
            info: meta.description,
            format: meta.format,
            minValue: meta.min,
            maxValue: meta.max,
            defaultValue: meta.default,
            readonly: !meta.writeable,
        });
    }
    return ret;
}
function createNotificationCCCapabilitiesFromDump(dump) {
    const supportsV1Alarm = findDumpedValue(dump, CommandClasses.Notification, NotificationCCValues.supportsV1Alarm.id, false);
    const ret = {
        supportsV1Alarm,
        notificationTypesAndEvents: {},
    };
    const supportedNotificationTypes = findDumpedValue(dump, CommandClasses.Notification, NotificationCCValues.supportedNotificationTypes.id, []);
    for (const type of supportedNotificationTypes) {
        const supportedEvents = findDumpedValue(dump, CommandClasses.Notification, NotificationCCValues.supportedNotificationEvents(type).id, []);
        ret.notificationTypesAndEvents[type] = supportedEvents;
    }
    return ret;
}
function createBinarySwitchCCCapabilitiesFromDump(dump) {
    const defaultValue = findDumpedValue(dump, CommandClasses["Binary Switch"], BinarySwitchCCValues.currentValue.id, undefined);
    return {
        defaultValue,
    };
}
function createMultilevelSwitchCCCapabilitiesFromDump(dump) {
    const defaultValue = findDumpedValue(dump, CommandClasses["Multilevel Switch"], MultilevelSwitchCCValues.currentValue.id, undefined);
    const switchType = findDumpedValue(dump, CommandClasses["Multilevel Switch"], MultilevelSwitchCCValues.switchType.id, SwitchType["Down/Up"]);
    return {
        defaultValue,
        primarySwitchType: switchType,
    };
}
function createSoundSwitchCCCapabilitiesFromDump(dump) {
    const defaultToneId = findDumpedValue(dump, CommandClasses["Sound Switch"], SoundSwitchCCValues.defaultToneId.id, 1);
    const defaultVolume = findDumpedValue(dump, CommandClasses["Sound Switch"], SoundSwitchCCValues.defaultVolume.id, 50);
    const ret = {
        defaultToneId,
        defaultVolume,
        tones: [],
    };
    const tonesMetadata = findDumpedMetadata(dump, CommandClasses["Sound Switch"], SoundSwitchCCValues.toneId.id);
    if (tonesMetadata?.states) {
        for (const [toneIdStr, nameAndDuration] of Object.entries(tonesMetadata.states)) {
            const toneId = parseInt(toneIdStr);
            if (Number.isNaN(toneId) || toneId < 1 || toneId > 0xfe)
                continue;
            const durationIndex = nameAndDuration.lastIndexOf("(");
            if (durationIndex === -1)
                continue;
            const name = nameAndDuration.slice(0, durationIndex).trim();
            const duration = parseInt(nameAndDuration.slice(durationIndex + 1, -1), 10);
            if (Number.isNaN(duration))
                continue;
            ret.tones.push({ name, duration });
        }
    }
    return ret;
}
function findDumpedValue(dump, commandClass, valueId, defaultValue) {
    return (dump.values.find((id) => id.property === valueId.property
        && id.propertyKey === valueId.propertyKey)?.value) ?? defaultValue;
}
function findDumpedMetadata(dump, commandClass, valueId) {
    return dump.values.find((id) => id.property === valueId.property
        && id.propertyKey === valueId.propertyKey)?.metadata;
}
//# sourceMappingURL=mockServer.js.map