"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EthernetDevice = void 0;
const tslib_1 = require("tslib");
const uuid_1 = require("uuid");
const ethercat_1 = require("./ethercat");
const parameter_1 = require("./parameter");
const device_parameter_1 = require("./device-parameter");
/**
 * Represents a single Ethernet device.
 * Each Ethernet device hosts a web server, and the base URL uniquely identifies the device.
 */
class EthernetDevice {
    /**
     * Creates an instance of the class with the specified base URL.
     *
     * @param baseUrl - The base URL for accessing the device API.
     */
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
        /**
         * Default headers for all requests to the Integro device web server.
         */
        this.headers = {
            Accept: 'application/json',
            'Content-Type': 'application/json',
        };
    }
    /**
     * Clears the internal cache by resetting parameters and PDO entries.
     *
     * This method sets the `parameters`, `rxPdoEntries`, and `txPdoEntries` to `undefined`,
     * effectively clearing any stored configuration or data for these properties.
     */
    clearCache() {
        this.parameters = undefined;
        this.rxPdoEntries = undefined;
        this.txPdoEntries = undefined;
    }
    /**
     * Parses a single entry (array of strings) representing information about a parameter from the GET /odl endpoint.
     * For example, the following entry corresponds to "0x2111:1" (Encoder 1 feedback - Raw position):
     * [
     *   "2111",         // Index in hexadecimal format
     *   "1",            // Subindex in decimal format
     *   "Raw position", // Parameter name
     *   "0",            // Object code
     *   "7",            // Value type
     *   "87"            // Flags
     * ]
     *
     * @param entry - An array of strings containing the parameter information.
     * @returns An object of type EthernetDeviceParameter containing the parsed values.
     */
    parseEthernetDeviceParameterEntry(entry) {
        const index = parseInt(entry[0], 16);
        const subindex = parseInt(entry[1], 16);
        const name = entry[2];
        const objectCode = parseInt(entry[3], 16);
        const valueType = parseInt(entry[4], 16);
        const flags = parseInt(entry[5], 16);
        return { index, subindex, name, objectCode, valueType, flags };
    }
    /**
     * Retrieves the list of available parameters on the device.
     *
     * Command Code: CMD=0x05
     *
     * @param uid - An optional unique identifier for the request.
     * @returns A promise that resolves to the list of available parameters.
     */
    getParameters(uid) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const url = new URL('odl', this.baseUrl);
            url.searchParams.set('uid', uid !== null && uid !== void 0 ? uid : (0, uuid_1.v4)());
            const response = yield fetch(url, { headers: this.headers });
            if (!response.ok) {
                throw new Error(`Failed to retrieve the parameter list from ${url}`);
            }
            const { data } = (yield response.json());
            const parameters = data.map((entry) => this.parseEthernetDeviceParameterEntry(entry));
            return parameters;
        });
    }
    /**
     * Retrieves the cached parameters.
     * If the parameters are not cached, a request is made to the device to fetch them.
     *
     * @returns A promise that resolves to an array of cached parameters.
     */
    getCachedParameters() {
        var _a;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (this.parameters) {
                return this.parameters;
            }
            this.parameters = yield this.getParameters();
            return (_a = this.parameters) !== null && _a !== void 0 ? _a : [];
        });
    }
    /**
     * Retrieves and caches PDO entries, including index, subindex, bit length, and data type,
     * for efficient reuse in operations like sending and receiving PDO values.
     *
     * Note that requests to NetX must be made sequentially; otherwise, a fetch call in Node.js may time out, resulting in a 'ConnectTimeoutError'.
     *
     * @param key - Specifies which PDO entries to retrieve ('rxPdoEntries' or 'txPdoEntries').
     * @returns A promise that resolves to an array of cached PDO entries.
     */
    getCachedPdoEntries(key) {
        var _a, _b;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (this[key]) {
                return (_a = this[key]) !== null && _a !== void 0 ? _a : [];
            }
            const matcherFn = key === 'rxPdoEntries' ? parameter_1.isRxPdoIndex : parameter_1.isTxPdoIndex;
            const allParameters = yield this.getCachedParameters();
            const pdoParameters = allParameters.filter(({ index, subindex }) => matcherFn(index) && subindex !== 0);
            const entries = [];
            for (const pdoParameter of pdoParameters) {
                const value = yield this.upload(pdoParameter.index, pdoParameter.subindex);
                const [index, subindex, bitLen] = (0, device_parameter_1.parsePdoSubItemValue)(value);
                const parameter = allParameters.find((p) => p.index === index && p.subindex === subindex);
                const dataType = (0, ethercat_1.findBaseDataTypeByIndex)((_b = parameter === null || parameter === void 0 ? void 0 : parameter.valueType) !== null && _b !== void 0 ? _b : 0); // Ignoring potentially undefined base data type.
                entries.push({ index, subindex, bitLen, dataType });
            }
            this[key] = entries;
            return entries;
        });
    }
    /**
     * Uploads a single parameter value from the device.
     *
     * Command Code: CMD=0x01
     *
     * @param index - The index of the parameter to upload.
     * @param subindex - The subindex of the parameter to upload.
     * @param uid - An optional unique identifier for the request.
     * @returns - A promise that resolves with the uploaded parameter value.
     */
    upload(index, subindex, uid) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const url = new URL('sdo', this.baseUrl);
            url.searchParams.set('uid', uid !== null && uid !== void 0 ? uid : (0, uuid_1.v4)());
            url.searchParams.set('idx', index.toString(16));
            url.searchParams.set('sub', subindex.toString(16));
            const response = yield fetch(url, { headers: this.headers });
            if (!response.ok) {
                throw new Error(`Failed to upload the parameter value from ${url}`);
            }
            const { data } = yield response.json();
            const elements = JSON.parse(data);
            const parameters = yield this.getCachedParameters();
            if (parameters) {
                const parameter = parameters.find((p) => p.index === index && p.subindex === subindex);
                if (parameter) {
                    const dataType = (0, ethercat_1.findBaseDataTypeByIndex)(parameter.valueType);
                    if (dataType) {
                        const typedArray = new Uint8Array(elements);
                        const dataView = new DataView(typedArray.buffer);
                        if (dataType.name === 'BOOLEAN') {
                            return dataView.getUint8(0);
                        }
                        else if (dataType.name === 'INTEGER8') {
                            return dataView.getInt8(0);
                        }
                        else if (dataType.name === 'INTEGER16') {
                            return dataView.getInt16(0, true);
                        }
                        else if (dataType.name === 'INTEGER32') {
                            return dataView.getInt32(0, true);
                        }
                        else if (dataType.name === 'UNSIGNED8') {
                            return dataView.getUint8(0);
                        }
                        else if (dataType.name === 'UNSIGNED16') {
                            return dataView.getUint16(0, true);
                        }
                        else if (dataType.name === 'UNSIGNED32') {
                            return dataView.getUint32(0, true);
                        }
                        else if (dataType.name === 'REAL32') {
                            return dataView.getFloat32(0, true);
                        }
                        else if (dataType.name === 'VISIBLE_STRING') {
                            const decoder = new TextDecoder('utf-8');
                            const result = decoder.decode(dataView);
                            const text = result.replace(/[^\x20-\x7E]/g, ''); // Remove non-printable characters; retain only printable ASCII characters.
                            return text;
                        }
                    }
                }
            }
            return elements;
        });
    }
    /**
     * Downloads the specified parameter value to the device.
     *
     * Command Code: CMD=0x02
     *
     * @param index - The index of the parameter to be downloaded.
     * @param subindex - The subindex of the parameter to be downloaded.
     * @param value - The value to set for the parameter.
     * @param uid - An optional unique identifier for the request.
     * @returns A promise that resolves when the download is complete.
     */
    download(index, subindex, value, uid) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const url = new URL('sdo', this.baseUrl);
            let elements;
            if (typeof value === 'string') {
                const uint8Array = new Uint8Array(value.length);
                for (let i = 0; i < value.length; i++) {
                    uint8Array[i] = value.charCodeAt(i);
                }
                elements = [...uint8Array];
            }
            else if (typeof value === 'number') {
                const parameters = yield this.getCachedParameters();
                const parameter = parameters.find((p) => p.index === index && p.subindex === subindex);
                if (!parameter) {
                    throw new Error(`Device parameter with index ${index} and subindex ${subindex} was not found.`);
                }
                const dataType = (0, ethercat_1.findBaseDataTypeByIndex)(parameter.valueType);
                if (!dataType) {
                    throw new Error(`Base data type '${parameter.valueType}' for the device parameter with index ${index} and subindex ${subindex} was not found.`);
                }
                const buffer = new ArrayBuffer(dataType.bitSize / 8);
                const dataView = new DataView(buffer);
                if (dataType.name === 'BOOLEAN') {
                    dataView.setUint8(0, value);
                }
                else if (dataType.name === 'INTEGER8') {
                    dataView.setInt8(0, value);
                }
                else if (dataType.name === 'INTEGER16') {
                    dataView.setInt16(0, value, true);
                }
                else if (dataType.name === 'INTEGER32') {
                    dataView.setInt32(0, value, true);
                }
                else if (dataType.name === 'UNSIGNED8') {
                    dataView.setUint8(0, value);
                }
                else if (dataType.name === 'UNSIGNED16') {
                    dataView.setUint16(0, value, true);
                }
                else if (dataType.name === 'UNSIGNED32') {
                    dataView.setUint32(0, value, true);
                }
                else if (dataType.name === 'REAL32') {
                    dataView.setFloat32(0, value, true);
                }
                const uint8Array = new Uint8Array(buffer);
                elements = [...uint8Array];
            }
            else if (value instanceof Uint8Array) {
                elements = [...value];
            }
            else if (Array.isArray(value)) {
                elements = value.slice();
            }
            if (!elements) {
                throw new Error(`Failed to create an array of numbers from the provided value: ${value}.`);
            }
            const bodyData = {
                uid: uid !== null && uid !== void 0 ? uid : (0, uuid_1.v4)(),
                idx: index.toString(16),
                sub: subindex.toString(16),
                data: JSON.stringify(elements),
            };
            const response = yield fetch(url, { headers: this.headers, method: 'PUT', body: JSON.stringify(bodyData) });
            if (!response.ok) {
                throw new Error(`Failed to download the parameter value to ${url}`);
            }
            return;
        });
    }
    /**
     * Retrieves the EtherCAT-compatible state of the device as defined by {@link EthercatState}.
     *
     * Command Code: CMD=0x06
     *
     * @param uid - An optional unique identifier for the request.
     * @returns {Promise<number>} A promise that resolves to the current state as a number.
     */
    getState(uid) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const url = new URL('mode', this.baseUrl);
            url.searchParams.set('uid', uid !== null && uid !== void 0 ? uid : (0, uuid_1.v4)());
            const response = yield fetch(url, { headers: this.headers });
            if (!response.ok) {
                throw new Error(`Failed to retrieve the state (mode) from ${url}.`);
            }
            const { data } = yield response.json();
            const elements = JSON.parse(data);
            return elements[0];
        });
    }
    /**
     * Sets the EtherCAT-compatible state of the device as defined by {@link EthercatState}.
     *
     * Command Code: CMD=0x07
     *
     * @param state - The target state to set for the device, based on the {@link EthercatState} enumeration.
     * @param uid - An optional unique identifier for the request.
     * @returns A promise that resolves when the state has been successfully set.
     */
    setState(state, uid) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const url = new URL('mode', this.baseUrl);
            const elements = [state];
            const bodyData = {
                uid: uid !== null && uid !== void 0 ? uid : (0, uuid_1.v4)(),
                data: JSON.stringify(elements),
            };
            const response = yield fetch(url, { headers: this.headers, method: 'PUT', body: JSON.stringify(bodyData) });
            if (!response.ok) {
                throw new Error(`Failed to set the state (mode) for ${url}`);
            }
            return;
        });
    }
    /**
     * Resets or restarts both NetX firmware components: COM and CORE.
     * This operation is typically performed after completing a firmware installation.
     *
     * Note that the device will respond before the actual restart occurs,
     * so please allow a few seconds for the restart to complete.
     *
     * Command Code: CMD=0x09
     *
     * @param {boolean} [clearCache=true] - Specifies whether to clear the parameters and PDO entries cache during the reset.
     * @returns A promise that resolves when the reset request is complete.
     */
    reset(clearCache = true) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const url = new URL(this.baseUrl);
            const formData = new FormData();
            formData.append('uploadfile', 'upload_file');
            formData.append('resetdev', 'device_reset');
            const response = yield fetch(url, { method: 'POST', body: formData });
            if (!response.ok) {
                throw new Error(`Failed to reset the device.`);
            }
            if (clearCache) {
                this.clearCache();
            }
            return;
        });
    }
    /**
     * Reads a file from the specified device.
     *
     * Command Code: CMD=0x03
     *
     * @todo Implement the function to fetch the file data from the device.
     * @param filename - The name of the file to be read.
     * @param uid - An optional unique identifier for the request.
     * @returns A promise that resolves to a Uint8Array containing the file data.
     */
    readFile(filename, uid) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            console.log(filename, uid);
            return new Uint8Array();
        });
    }
    /**
     * Writes the specified file content to the device.
     *
     * Command Code: CMD=0x04
     *
     * @todo Implement the function to write the file content to the device.
     * @param filename - The name of the file to be created or overwritten on the device.
     * @param content - The file content to be written, provided as a Uint8Array.
     * @param uid - An optional unique identifier for the request.
     * @returns A promise that resolves when the file has been successfully written.
     */
    writeFile(filename, content, uid) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            console.log(filename, content, uid);
            return;
        });
    }
    /**
     * Send PDO values to the device.
     *
     * The provided array of values will be converted into a byte array based on parameters' data types.
     *
     * Command Code: CMD=0x0B
     *
     * @param values
     * @returns
     */
    sendPdoValues(values, uid) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const url = new URL('pdo', this.baseUrl);
            const pdoEntries = yield this.getCachedPdoEntries('rxPdoEntries');
            const totalLength = pdoEntries.reduce((n, entry) => n + entry.bitLen / 8, 0);
            const buffer = new ArrayBuffer(totalLength);
            const dataView = new DataView(buffer);
            let offset = 0;
            values.forEach((value, i) => {
                const { dataType } = pdoEntries[i];
                if (dataType.name === 'BOOLEAN') {
                    dataView.setUint8(offset, value);
                }
                else if (dataType.name === 'INTEGER8') {
                    dataView.setInt8(offset, value);
                }
                else if (dataType.name === 'INTEGER16') {
                    dataView.setInt16(offset, value, true);
                }
                else if (dataType.name === 'INTEGER32') {
                    dataView.setInt32(offset, value, true);
                }
                else if (dataType.name === 'UNSIGNED8') {
                    dataView.setUint8(offset, value);
                }
                else if (dataType.name === 'UNSIGNED16') {
                    dataView.setUint16(offset, value, true);
                }
                else if (dataType.name === 'UNSIGNED32') {
                    dataView.setUint32(offset, value, true);
                }
                else if (dataType.name === 'REAL32') {
                    dataView.setFloat32(offset, value, true);
                }
                offset += dataType.bitSize / 8;
            });
            const byteArray = new Uint8Array(buffer);
            const bytes = Array.from(byteArray);
            const bodyData = {
                uid: uid !== null && uid !== void 0 ? uid : (0, uuid_1.v4)(),
                data: JSON.stringify(bytes),
            };
            const response = yield fetch(url, { headers: this.headers, method: 'PUT', body: JSON.stringify(bodyData) });
            if (!response.ok) {
                throw new Error(`Failed to send PDO values to ${url}.`);
            }
            return;
        });
    }
    /**
     * Receives PDO values from the device.
     *
     * The device sends an array of bytes, and this function processes the parameters, their data types,
     * and the PDO entries to convert the byte array into an array of received PDO entry values.
     *
     * Command Code: CMD=0x0A
     *
     * @param uid - An optional unique identifier for the request.
     * @returns An array of received PDO entry values.
     */
    receivePdoValues(uid) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const url = new URL('pdo', this.baseUrl);
            url.searchParams.set('uid', uid !== null && uid !== void 0 ? uid : (0, uuid_1.v4)());
            const response = yield fetch(url, { headers: this.headers });
            if (!response.ok) {
                throw new Error(`Failed to receive PDO values from ${url}.`);
            }
            const pdoEntries = yield this.getCachedPdoEntries('txPdoEntries');
            const { data } = yield response.json();
            const bytes = JSON.parse(data);
            const uint8Array = new Uint8Array(bytes);
            const dataView = new DataView(uint8Array.buffer);
            const values = [];
            let offset = 0;
            pdoEntries.forEach(({ dataType }) => {
                let value = 0;
                if (dataType.name === 'BOOLEAN') {
                    value = dataView.getUint8(offset);
                }
                else if (dataType.name === 'INTEGER8') {
                    value = dataView.getInt8(offset);
                }
                else if (dataType.name === 'INTEGER16') {
                    value = dataView.getInt16(offset, true);
                }
                else if (dataType.name === 'INTEGER32') {
                    value = dataView.getInt32(offset, true);
                }
                else if (dataType.name === 'UNSIGNED8') {
                    value = dataView.getUint8(offset);
                }
                else if (dataType.name === 'UNSIGNED16') {
                    value = dataView.getUint16(offset, true);
                }
                else if (dataType.name === 'UNSIGNED32') {
                    value = dataView.getUint32(offset, true);
                }
                else if (dataType.name === 'REAL32') {
                    value = dataView.getFloat32(offset, true);
                }
                values.push(value);
                offset += dataType.bitSize / 8;
            });
            return values;
        });
    }
    /**
     * Uploads the firmware file to the device. Call the reset method to apply the changes.
     *
     * @todo This function has not been tested. Verify its functionality by uploading a firmware file.
     * @param buffer - The firmware data to upload.
     * @param filename - The name of the firmware file.
     * @returns A promise that resolves when the upload is complete.
     */
    uploadFirmware(buffer, filename) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const url = new URL(this.baseUrl);
            const blob = new Blob([buffer], { type: 'application/octet-stream' });
            const formData = new FormData();
            formData.append('uploadfile', blob, filename);
            formData.append('upload', 'upload');
            const response = yield fetch(url, {
                method: 'POST',
                body: formData,
            });
            if (!response.ok) {
                throw new Error(`Failed to reset the device.`);
            }
            return;
        });
    }
    /**
     * Updates the firmware by uploading it to the device and restarting the NetX.
     *
     * @todo This function has not been tested. Verify its functionality by uploading a firmware file.
     * @param buffer - The firmware data to upload.
     * @param filename - The name of the firmware file.
     */
    updateFirmware(buffer, filename) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            yield this.uploadFirmware(buffer, filename);
            yield this.reset(true);
        });
    }
}
exports.EthernetDevice = EthernetDevice;
//# sourceMappingURL=ethernet.js.map