"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDataTypeClass = getDataTypeClass;
exports.getCluster = getCluster;
exports.getGlobalCommand = getGlobalCommand;
exports.isClusterName = isClusterName;
exports.getFoundationCommand = getFoundationCommand;
exports.isFoundationDiscoverRsp = isFoundationDiscoverRsp;
exports.processAttributeWrite = processAttributeWrite;
exports.processAttributePreRead = processAttributePreRead;
exports.processAttributePostRead = processAttributePostRead;
exports.processParameterWrite = processParameterWrite;
exports.processParameterRead = processParameterRead;
const cluster_1 = require("./definition/cluster");
const datatypes_1 = require("./definition/datatypes");
const enums_1 = require("./definition/enums");
const foundation_1 = require("./definition/foundation");
const DATA_TYPE_CLASS_DISCRETE = [
    enums_1.DataType.DATA8,
    enums_1.DataType.DATA16,
    enums_1.DataType.DATA24,
    enums_1.DataType.DATA32,
    enums_1.DataType.DATA40,
    enums_1.DataType.DATA48,
    enums_1.DataType.DATA56,
    enums_1.DataType.DATA64,
    enums_1.DataType.BOOLEAN,
    enums_1.DataType.BITMAP8,
    enums_1.DataType.BITMAP16,
    enums_1.DataType.BITMAP24,
    enums_1.DataType.BITMAP32,
    enums_1.DataType.BITMAP40,
    enums_1.DataType.BITMAP48,
    enums_1.DataType.BITMAP56,
    enums_1.DataType.BITMAP64,
    enums_1.DataType.ENUM8,
    enums_1.DataType.ENUM16,
    enums_1.DataType.OCTET_STR,
    enums_1.DataType.CHAR_STR,
    enums_1.DataType.LONG_OCTET_STR,
    enums_1.DataType.LONG_CHAR_STR,
    enums_1.DataType.ARRAY,
    enums_1.DataType.STRUCT,
    enums_1.DataType.SET,
    enums_1.DataType.BAG,
    enums_1.DataType.CLUSTER_ID,
    enums_1.DataType.ATTR_ID,
    enums_1.DataType.BAC_OID,
    enums_1.DataType.IEEE_ADDR,
    enums_1.DataType.SEC_KEY,
];
const DATA_TYPE_CLASS_ANALOG = [
    enums_1.DataType.UINT8,
    enums_1.DataType.UINT16,
    enums_1.DataType.UINT24,
    enums_1.DataType.UINT32,
    enums_1.DataType.UINT40,
    enums_1.DataType.UINT48,
    enums_1.DataType.UINT56,
    enums_1.DataType.INT8,
    enums_1.DataType.INT16,
    enums_1.DataType.INT24,
    enums_1.DataType.INT32,
    enums_1.DataType.INT40,
    enums_1.DataType.INT48,
    enums_1.DataType.INT56,
    enums_1.DataType.SEMI_PREC,
    enums_1.DataType.SINGLE_PREC,
    enums_1.DataType.DOUBLE_PREC,
    enums_1.DataType.TOD,
    enums_1.DataType.DATE,
    enums_1.DataType.UTC,
];
const FOUNDATION_DISCOVER_RSP_IDS = [
    foundation_1.Foundation.discoverRsp.ID,
    foundation_1.Foundation.discoverCommandsRsp.ID,
    foundation_1.Foundation.discoverCommandsGenRsp.ID,
    foundation_1.Foundation.discoverExtRsp.ID,
];
/** Runtime fast lookup */
const ZCL_CLUSTERS_ID_TO_NAMES = (() => {
    const map = new Map();
    for (const clusterName in cluster_1.Clusters) {
        const cluster = cluster_1.Clusters[clusterName];
        map.set(cluster.ID, clusterName);
    }
    return map;
})();
function getDataTypeClass(dataType) {
    if (DATA_TYPE_CLASS_DISCRETE.includes(dataType)) {
        return enums_1.DataTypeClass.DISCRETE;
    }
    if (DATA_TYPE_CLASS_ANALOG.includes(dataType)) {
        return enums_1.DataTypeClass.ANALOG;
    }
    throw new Error(`Don't know value type for '${enums_1.DataType[dataType]}'`);
}
function hasCustomClusters(customClusters) {
    // XXX: was there a good reason to not set the parameter `customClusters` optional? it would allow simple undefined check
    // below is twice faster than checking `Object.keys(customClusters).length`
    for (const _k in customClusters)
        return true;
    return false;
}
function findClusterNameByID(id, manufacturerCode, clusters, zcl) {
    let name;
    // if manufacturer code is given, consider partial match if didn't match against manufacturer code
    let partialMatch = Boolean(manufacturerCode);
    if (zcl) {
        const zclName = ZCL_CLUSTERS_ID_TO_NAMES.get(id);
        if (zclName) {
            const cluster = clusters[zclName];
            // priority on first match when matching only ID
            if (name === undefined) {
                name = zclName;
            }
            if (manufacturerCode && cluster.manufacturerCode === manufacturerCode) {
                name = zclName;
                partialMatch = false;
            }
            else if (!cluster.manufacturerCode) {
                name = zclName;
            }
        }
    }
    else {
        for (const clusterName in clusters) {
            const cluster = clusters[clusterName];
            if (cluster.ID === id) {
                // priority on first match when matching only ID
                if (name === undefined) {
                    name = clusterName;
                }
                if (manufacturerCode && cluster.manufacturerCode === manufacturerCode) {
                    name = clusterName;
                    partialMatch = false;
                    break;
                }
                if (!cluster.manufacturerCode) {
                    name = clusterName;
                    break;
                }
            }
        }
    }
    return [name, partialMatch];
}
function getClusterDefinition(key, manufacturerCode, customClusters) {
    let name;
    if (typeof key === "number") {
        let partialMatch;
        // custom clusters have priority over Zcl clusters, except in case of better match (see below)
        [name, partialMatch] = findClusterNameByID(key, manufacturerCode, customClusters, false);
        if (!name) {
            [name, partialMatch] = findClusterNameByID(key, manufacturerCode, cluster_1.Clusters, true);
        }
        else if (partialMatch) {
            let zclName;
            [zclName, partialMatch] = findClusterNameByID(key, manufacturerCode, cluster_1.Clusters, true);
            // Zcl clusters contain a better match, use that one
            if (zclName !== undefined && !partialMatch) {
                name = zclName;
            }
        }
    }
    else {
        name = key;
    }
    let cluster = name !== undefined && hasCustomClusters(customClusters)
        ? {
            ...cluster_1.Clusters[name],
            ...customClusters[name], // should override Zcl clusters
        }
        : cluster_1.Clusters[name];
    if (!cluster || cluster.ID === undefined) {
        if (typeof key === "number") {
            name = key.toString();
            cluster = { attributes: {}, commands: {}, commandsResponse: {}, manufacturerCode: undefined, ID: key };
        }
        else {
            name = undefined;
        }
    }
    if (!name) {
        throw new Error(`Cluster with name '${key}' does not exist`);
    }
    return { name, cluster };
}
function cloneClusterEntriesWithName(entries) {
    const clone = {};
    for (const key in entries) {
        clone[key] = { ...entries[key], name: key };
    }
    return clone;
}
function createCluster(name, cluster, manufacturerCode) {
    const attributes = cloneClusterEntriesWithName(cluster.attributes);
    const commands = cloneClusterEntriesWithName(cluster.commands);
    const commandsResponse = cloneClusterEntriesWithName(cluster.commandsResponse);
    const getAttribute = (key) => {
        if (typeof key === "number") {
            let partialMatchAttr;
            for (const attrKey in attributes) {
                const attr = attributes[attrKey];
                if (attr.ID === key) {
                    if (manufacturerCode && attr.manufacturerCode === manufacturerCode) {
                        return attr;
                    }
                    if (attr.manufacturerCode === undefined) {
                        partialMatchAttr = attr;
                    }
                }
            }
            return partialMatchAttr;
        }
        return attributes[key];
    };
    const getCommand = (key) => {
        if (typeof key === "number") {
            for (const cmdKey in commands) {
                const cmd = commands[cmdKey];
                if (cmd.ID === key) {
                    return cmd;
                }
            }
        }
        else {
            const cmd = commands[key];
            if (cmd) {
                return cmd;
            }
        }
        throw new Error(`Cluster '${name}' has no command '${key}'`);
    };
    const getCommandResponse = (key) => {
        if (typeof key === "number") {
            for (const cmdKey in commandsResponse) {
                const cmd = commandsResponse[cmdKey];
                if (cmd.ID === key) {
                    return cmd;
                }
            }
        }
        else {
            const cmd = commandsResponse[key];
            if (cmd) {
                return cmd;
            }
        }
        throw new Error(`Cluster '${name}' has no command response '${key}'`);
    };
    return {
        ID: cluster.ID,
        attributes,
        manufacturerCode: cluster.manufacturerCode,
        name,
        commands,
        commandsResponse,
        getAttribute,
        getCommand,
        getCommandResponse,
    };
}
function getCluster(key, manufacturerCode = undefined, customClusters = {}) {
    const { name, cluster } = getClusterDefinition(key, manufacturerCode, customClusters);
    return createCluster(name, cluster, manufacturerCode);
}
function getGlobalCommandNameById(id) {
    for (const commandName in foundation_1.Foundation) {
        if (foundation_1.Foundation[commandName].ID === id) {
            return commandName;
        }
    }
    throw new Error(`Global command with id '${id}' does not exist.`);
}
function getGlobalCommand(key) {
    const name = typeof key === "number" ? getGlobalCommandNameById(key) : key;
    const command = foundation_1.Foundation[name];
    if (!command) {
        throw new Error(`Global command with key '${key}' does not exist`);
    }
    const result = {
        ID: command.ID,
        name,
        parameters: command.parameters,
    };
    if (command.response !== undefined) {
        result.response = command.response;
    }
    return result;
}
function isClusterName(name) {
    return name in cluster_1.Clusters;
}
function getFoundationCommand(id) {
    for (const commandName in foundation_1.Foundation) {
        const command = foundation_1.Foundation[commandName];
        if (command.ID === id) {
            return command;
        }
    }
    throw new Error(`Foundation command '${id}' does not exist.`);
}
function isFoundationDiscoverRsp(id) {
    return FOUNDATION_DISCOVER_RSP_IDS.includes(id);
}
/** Check if value is equal to either min, max, minRef or maxRef */
function isMinOrMax(entry, value) {
    if (value === entry.max || value === entry.min) {
        return true;
    }
    return false;
}
function processRestrictions(entry, value) {
    if (entry.min !== undefined && value < entry.min) {
        throw new Error(`${entry.name} requires min of ${entry.min}`);
    }
    if (entry.minExcl !== undefined && value <= entry.minExcl) {
        throw new Error(`${entry.name} requires min exclusive of ${entry.minExcl}`);
    }
    if (entry.max !== undefined && value > entry.max) {
        throw new Error(`${entry.name} requires max of ${entry.max}`);
    }
    if (entry.maxExcl !== undefined && value >= entry.maxExcl) {
        throw new Error(`${entry.name} requires max exclusive of ${entry.maxExcl}`);
    }
    if (entry.length !== undefined && value.length !== entry.length) {
        throw new Error(`${entry.name} requires length of ${entry.length}`);
    }
    if (entry.minLen !== undefined && value.length < entry.minLen) {
        throw new Error(`${entry.name} requires min length of ${entry.minLen}`);
    }
    if (entry.maxLen !== undefined && value.length > entry.maxLen) {
        throw new Error(`${entry.name} requires max length of ${entry.maxLen}`);
    }
}
function processAttributeWrite(attribute, value) {
    if (attribute.write !== true) {
        throw new Error(`Attribute ${attribute.name} (${attribute.ID}) is not writable`);
    }
    if (value == null) {
        return attribute.default !== undefined ? attribute.default : value /* XXX: dangerous fallback */;
    }
    // if default, always valid
    if (attribute.default === value) {
        return value;
    }
    if (Number.isNaN(value)) {
        if (attribute.default === undefined) {
            const nonValue = datatypes_1.ZCL_TYPE_INVALID_BY_TYPE[attribute.type];
            if (nonValue === undefined) {
                throw new Error(`Attribute ${attribute.name} (${attribute.ID}) does not have a default nor a non-value`);
            }
            return nonValue;
        }
        return attribute.default;
    }
    processRestrictions(attribute, value);
    return value;
}
function processAttributePreRead(attribute) {
    if (attribute.read === false) {
        throw new Error(`Attribute ${attribute.name} (${attribute.ID}) is not readable`);
    }
}
function processAttributePostRead(attribute, value) {
    // should never happen?
    if (value == null) {
        return value;
    }
    // if default, always valid
    if (attribute.default === value) {
        return value;
    }
    // if type does not have an `invalid` (undefined) it won't match since value is checked above
    if (value === datatypes_1.ZCL_TYPE_INVALID_BY_TYPE[attribute.type]) {
        // if value is same as max or min, ignore invalid sentinel
        if (isMinOrMax(attribute, value)) {
            return value;
        }
        // return NaN for both number & bigint to keep logic consistent
        return Number.NaN;
    }
    processRestrictions(attribute, value);
    return value;
}
function processParameterWrite(parameter, value) {
    // should never happen?
    if (value == null) {
        return value;
    }
    if (Number.isNaN(value)) {
        const nonValue = datatypes_1.ZCL_TYPE_INVALID_BY_TYPE[parameter.type];
        if (nonValue === undefined) {
            throw new Error(`Parameter ${parameter.name} does not have a non-value`);
        }
        return nonValue;
    }
    processRestrictions(parameter, value);
    return value;
}
function processParameterRead(parameter, value) {
    // should never happen?
    if (value == null) {
        return value;
    }
    // if type does not have an `invalid` (undefined) it won't match since value is checked above
    if (value === datatypes_1.ZCL_TYPE_INVALID_BY_TYPE[parameter.type]) {
        // if value is same as max or min, ignore invalid sentinel
        if (isMinOrMax(parameter, value)) {
            return value;
        }
        // return NaN for both number & bigint to keep logic consistent
        return Number.NaN;
    }
    processRestrictions(parameter, value);
    return value;
}
//# sourceMappingURL=utils.js.map