import { ZWaveError, ZWaveErrorCodes, getNodeTag, highResTimestamp, } from "@zwave-js/core";
import { createReflectionDecorator } from "@zwave-js/core/reflection";
import { Bytes, num2hex, staticExtends, } from "@zwave-js/shared";
import { FunctionType, MessageType } from "./Constants.js";
import { MessageHeaders } from "./MessageHeaders.js";
/** Where a serialized message originates from, to distinguish how certain messages need to be deserialized */
export var MessageOrigin;
(function (MessageOrigin) {
    MessageOrigin[MessageOrigin["Controller"] = 0] = "Controller";
    MessageOrigin[MessageOrigin["Host"] = 1] = "Host";
})(MessageOrigin || (MessageOrigin = {}));
/** Tests if the given message is for a node or references a node */
export function hasNodeId(msg) {
    return typeof msg.nodeId === "number";
}
/** Returns the number of bytes the first message in the buffer occupies */
function getMessageLength(data) {
    const remainingLength = data[1];
    return remainingLength + 2;
}
export class MessageRaw {
    type;
    functionType;
    payload;
    constructor(type, functionType, payload) {
        this.type = type;
        this.functionType = functionType;
        this.payload = payload;
    }
    static parse(data) {
        // SOF, length, type, commandId and checksum must be present
        if (!data.length || data.length < 5) {
            throw new ZWaveError("Could not deserialize the message because it was truncated", ZWaveErrorCodes.PacketFormat_Truncated);
        }
        // the packet has to start with SOF
        if (data[0] !== MessageHeaders.SOF) {
            throw new ZWaveError("Could not deserialize the message because it does not start with SOF", ZWaveErrorCodes.PacketFormat_Invalid);
        }
        // check the length again, this time with the transmitted length
        const messageLength = getMessageLength(data);
        if (data.length < messageLength) {
            throw new ZWaveError("Could not deserialize the message because it was truncated", ZWaveErrorCodes.PacketFormat_Truncated);
        }
        // check the checksum
        const expectedChecksum = computeChecksum(data.subarray(0, messageLength));
        if (data[messageLength - 1] !== expectedChecksum) {
            throw new ZWaveError("Could not deserialize the message because the checksum didn't match", ZWaveErrorCodes.PacketFormat_Checksum);
        }
        const type = data[2];
        const functionType = data[3];
        const payloadLength = messageLength - 5;
        const payload = Bytes.view(data.subarray(4, 4 + payloadLength));
        return new MessageRaw(type, functionType, payload);
    }
    withPayload(payload) {
        return new MessageRaw(this.type, this.functionType, payload);
    }
}
/**
 * Represents a Z-Wave message for communication with the serial interface
 */
export class Message {
    constructor(options = {}) {
        const { 
        // Try to determine the message type if none is given
        type = getMessageType(this), 
        // Try to determine the function type if none is given
        functionType = getFunctionType(this), 
        // Fall back to decorated response/callback types if none is given
        expectedResponse = getExpectedResponse(this), expectedCallback = getExpectedCallback(this), payload = new Bytes(), callbackId, } = options;
        if (type == undefined) {
            throw new ZWaveError("A message must have a given or predefined message type", ZWaveErrorCodes.Argument_Invalid);
        }
        if (functionType == undefined) {
            throw new ZWaveError("A message must have a given or predefined function type", ZWaveErrorCodes.Argument_Invalid);
        }
        this.type = type;
        this.functionType = functionType;
        this.expectedResponse = expectedResponse;
        this.expectedCallback = expectedCallback;
        this.callbackId = callbackId;
        this.payload = payload;
    }
    static parse(data, ctx) {
        const raw = MessageRaw.parse(data);
        const Constructor = getMessageConstructor(raw.type, raw.functionType)
            ?? Message;
        return Constructor.from(raw, ctx);
    }
    /** Creates an instance of the message that is serialized in the given buffer */
    static from(raw, 
    // oxlint-disable-next-line no-unused-vars
    ctx) {
        return new this({
            type: raw.type,
            functionType: raw.functionType,
            payload: raw.payload,
        });
    }
    type;
    functionType;
    expectedResponse;
    expectedCallback;
    payload; // TODO: Length limit 255
    /** Used to map requests to callbacks */
    callbackId;
    assertCallbackId() {
        if (this.callbackId == undefined) {
            throw new ZWaveError("Callback ID required but not set", ZWaveErrorCodes.PacketFormat_Invalid);
        }
    }
    /** Returns whether the callback ID is set */
    hasCallbackId() {
        return this.callbackId != undefined;
    }
    /**
     * Tests whether this message needs a callback ID to match its response
     */
    needsCallbackId() {
        return true;
    }
    /** Returns the response timeout for this message in case the default settings do not apply. */
    getResponseTimeout() {
        // Use default timeout
        return;
    }
    /** Returns the callback timeout for this message in case the default settings do not apply. */
    getCallbackTimeout() {
        // Use default timeout
        return;
    }
    /**
     * Serializes this message into a Buffer
     */
    // oxlint-disable-next-line no-unused-vars, @typescript-eslint/require-await
    async serialize(ctx) {
        const ret = new Bytes(this.payload.length + 5);
        ret[0] = MessageHeaders.SOF;
        // length of the following data, including the checksum
        ret[1] = this.payload.length + 3;
        // write the remaining data
        ret[2] = this.type;
        ret[3] = this.functionType;
        ret.set(this.payload, 4);
        // followed by the checksum
        ret[ret.length - 1] = computeChecksum(ret);
        return ret;
    }
    /** Generates a representation of this Message for the log */
    toLogEntry() {
        const tags = [
            this.type === MessageType.Request ? "REQ" : "RES",
            FunctionType[this.functionType],
        ];
        const nodeId = this.getNodeId();
        if (nodeId)
            tags.unshift(getNodeTag(nodeId));
        return {
            tags,
            message: this.payload.length > 0
                ? { payload: `0x${this.payload.toString("hex")}` }
                : undefined,
        };
    }
    /** Generates the JSON representation of this Message */
    toJSON() {
        return this.toJSONInternal();
    }
    toJSONInternal() {
        const ret = {
            name: this.constructor.name,
            type: MessageType[this.type],
            functionType: FunctionType[this.functionType]
                || num2hex(this.functionType),
        };
        if (this.expectedResponse != null) {
            ret.expectedResponse = FunctionType[this.functionType];
        }
        ret.payload = this.payload.toString("hex");
        return ret;
    }
    testMessage(msg, predicate) {
        if (predicate == undefined)
            return false;
        if (typeof predicate === "number") {
            return msg.functionType === predicate;
        }
        if (staticExtends(predicate, Message)) {
            // predicate is a Message constructor
            return msg instanceof predicate;
        }
        else {
            // predicate is a ResponsePredicate
            return predicate(this, msg);
        }
    }
    /** Tests whether this message expects an ACK from the controller */
    expectsAck() {
        // By default, all commands expect an ACK
        return true;
    }
    /** Tests whether this message expects a response from the controller */
    expectsResponse() {
        return !!this.expectedResponse;
    }
    /** Tests whether this message expects a callback from the controller */
    expectsCallback() {
        // A message expects a callback...
        return (
        // ...when it has a callback id that is not 0 (no callback)
        ((this.hasCallbackId() && this.callbackId !== 0)
            // or the message type does not need a callback id to match the response
            || !this.needsCallbackId())
            // and the expected callback is defined
            && !!this.expectedCallback);
    }
    /** Tests whether this message expects an update from the target node to finalize the transaction */
    // oxlint-disable-next-line no-unused-vars
    expectsNodeUpdate(ctx) {
        // Most messages don't expect an update by default
        return false;
    }
    /** Returns a message specific timeout used to wait for an update from the target node */
    nodeUpdateTimeout; // Default: use driver timeout
    /** Checks if a message is an expected response for this message */
    isExpectedResponse(msg) {
        return (msg.type === MessageType.Response
            && this.testMessage(msg, this.expectedResponse));
    }
    /** Checks if a message is an expected callback for this message */
    isExpectedCallback(msg) {
        if (msg.type !== MessageType.Request)
            return false;
        // Some controllers have a bug causing them to send a callback with a function type of 0 and no callback ID
        // To prevent this from triggering the unresponsive controller detection we need to forward these messages as if they were correct
        if (msg.functionType !== 0) {
            // If a received request included a callback id, enforce that the response contains the same
            if (this.callbackId !== msg.callbackId) {
                return false;
            }
        }
        return this.testMessage(msg, this.expectedCallback);
    }
    /** Checks if a message is an expected node update for this message */
    isExpectedNodeUpdate(
    // oxlint-disable-next-line no-unused-vars
    ctx, 
    // oxlint-disable-next-line no-unused-vars
    msg) {
        // Most messages don't expect an update by default
        return false;
    }
    /** Gets set by the driver to remember an expected node update for this message that arrived before the Serial API command has finished. */
    prematureNodeUpdate;
    /** Finds the ID of the target or source node in a message, if it contains that information */
    getNodeId() {
        if (hasNodeId(this))
            return this.nodeId;
        // Override this in subclasses if a different behavior is desired
    }
    /**
     * Returns the node this message is linked to or undefined
     */
    tryGetNode(ctx) {
        const nodeId = this.getNodeId();
        if (nodeId != undefined)
            return ctx.getNode(nodeId);
    }
    _transmissionTimestamp;
    /** The timestamp when this message was (last) transmitted (in nanoseconds) */
    get transmissionTimestamp() {
        return this._transmissionTimestamp;
    }
    /** Marks this message as sent and sets the transmission timestamp */
    markAsSent() {
        this._transmissionTimestamp = highResTimestamp();
        this._completedTimestamp = undefined;
    }
    _completedTimestamp;
    get completedTimestamp() {
        return this._completedTimestamp;
    }
    /** Marks this message as completed and sets the corresponding timestamp */
    markAsCompleted() {
        this._completedTimestamp = highResTimestamp();
    }
    /** Returns the round trip time of this message from transmission until completion. */
    get rtt() {
        if (this._transmissionTimestamp == undefined)
            return undefined;
        if (this._completedTimestamp == undefined)
            return undefined;
        return this._completedTimestamp - this._transmissionTimestamp;
    }
}
/** Computes the checksum for a serialized message as defined in the Z-Wave specs */
function computeChecksum(message) {
    let ret = 0xff;
    // exclude SOF and checksum byte from the computation
    for (let i = 1; i < message.length - 1; i++) {
        ret ^= message[i];
    }
    return ret;
}
function getMessageTypeMapKey(messageType, functionType) {
    return JSON.stringify({ messageType, functionType });
}
const messageTypesDecorator = createReflectionDecorator({
    name: "messageTypes",
    valueFromArgs: (messageType, functionType) => ({
        messageType,
        functionType,
    }),
    constructorLookupKey(target, messageType, functionType) {
        return getMessageTypeMapKey(messageType, functionType);
    },
});
/**
 * Defines the message and function type associated with a Z-Wave message
 */
export const messageTypes = messageTypesDecorator.decorator;
/**
 * Retrieves the message type defined for a Z-Wave message class
 */
export function getMessageType(messageClass) {
    return messageTypesDecorator.lookupValue(messageClass)?.messageType;
}
/**
 * Retrieves the message type defined for a Z-Wave message class
 */
export function getMessageTypeStatic(classConstructor) {
    return messageTypesDecorator.lookupValueStatic(classConstructor)
        ?.messageType;
}
/**
 * Retrieves the function type defined for a Z-Wave message class
 */
export function getFunctionType(messageClass) {
    return messageTypesDecorator.lookupValue(messageClass)?.functionType;
}
/**
 * Retrieves the function type defined for a Z-Wave message class
 */
export function getFunctionTypeStatic(classConstructor) {
    return messageTypesDecorator.lookupValueStatic(classConstructor)
        ?.functionType;
}
/**
 * Looks up the message constructor for a given message type and function type
 */
function getMessageConstructor(messageType, functionType) {
    return messageTypesDecorator.lookupConstructorByKey(getMessageTypeMapKey(messageType, functionType));
}
const expectedResponseDecorator = createReflectionDecorator({
    name: "expectedResponse",
    valueFromArgs: (typeOrPredicate) => typeOrPredicate,
    constructorLookupKey: false,
});
/**
 * Defines the expected response function type or message class for a Z-Wave message
 */
export const expectedResponse = expectedResponseDecorator.decorator;
/**
 * Retrieves the expected response function type or message class defined for a Z-Wave message class
 */
export function getExpectedResponse(messageClass) {
    return expectedResponseDecorator.lookupValue(messageClass);
}
/**
 * Retrieves the function type defined for a Z-Wave message class
 */
export function getExpectedResponseStatic(classConstructor) {
    return expectedResponseDecorator.lookupValueStatic(classConstructor);
}
const expectedCallbackDecorator = createReflectionDecorator({
    name: "expectedCallback",
    valueFromArgs: (typeOrPredicate) => typeOrPredicate,
    constructorLookupKey: false,
});
/**
 * Defines the expected callback function type or message class for a Z-Wave message
 */
export function expectedCallback(typeOrPredicate) {
    return expectedCallbackDecorator.decorator(typeOrPredicate);
}
/**
 * Retrieves the expected callback function type or message class defined for a Z-Wave message class
 */
export function getExpectedCallback(messageClass) {
    return expectedCallbackDecorator.lookupValue(messageClass);
}
/**
 * Retrieves the function type defined for a Z-Wave message class
 */
export function getExpectedCallbackStatic(classConstructor) {
    return expectedCallbackDecorator.lookupValueStatic(classConstructor);
}
const priorityDecorator = createReflectionDecorator({
    name: "priority",
    valueFromArgs: (priority) => priority,
    constructorLookupKey: false,
});
/**
 * Defines the default priority associated with a Z-Wave message
 */
export const priority = priorityDecorator.decorator;
/**
 * Retrieves the default priority defined for a Z-Wave message class
 */
export function getDefaultPriority(messageClass) {
    return priorityDecorator.lookupValue(messageClass);
}
/**
 * Retrieves the default priority defined for a Z-Wave message class
 */
export function getDefaultPriorityStatic(classConstructor) {
    return priorityDecorator.lookupValueStatic(classConstructor);
}
//# sourceMappingURL=Message.js.map