import { GcConsole } from '../../gc-core-assets/lib/GcConsole';
import { GcPromise } from '../../gc-core-assets/lib/GcPromise';
import { ServiceType, ServicesRegistry } from '../../gc-core-services/lib/ServicesRegistry';
import { EventType, Events } from '../../gc-core-assets/lib/Events';
import { usbServiceType, usbSerialPortType, dataEventType } from '../../gc-service-usb/lib/ServiceUsb';

/**
 *  Copyright (c) 2025 Texas Instruments Incorporated
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *
 *  *   Redistributions of source code must retain the above copyright
 *  notice, this list of conditions and the following disclaimer.
 *  notice, this list of conditions and the following disclaimer in the
 *  documentation and/or other materials provided with the distribution.
 *  *   Neither the name of Texas Instruments Incorporated nor the names of
 *  its contributors may be used to endorse or promote products derived
 *  from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 *  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 *  OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 *  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 *  OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 *  EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
const DEFAULT_TIMEOUT_MS = 3500;
const DEFAULT_BAUD = 115200;
//  ----------------------  XModem Service  ----------------------
const XMODEM_SERVICE_MODULE_NAME = 'gc-service-xmodem';
const xmodemServiceType = new ServiceType(XMODEM_SERVICE_MODULE_NAME);
/**
 * Xmodem service.
 *
 * The XmodemService is a singleton that provides a method to identify all
 * available IXmodemPorts. It implements the IXmodemService interface.
 */
class XmodemService {
    /**
     * Constructs a new instance of XmodemService
     */
    constructor() {
    }
    /**
     * Identifies all USB serial ports broadcasting Xmodem ready signals.
     *
     * @param timeoutMS The timeout in milliseconds.
     * @param baud      The baud rate
     * @returns         Promise resolving to the identified ports array.
     */
    async listPorts(timeoutMs = DEFAULT_TIMEOUT_MS, baud = DEFAULT_BAUD) {
        const heartBeatByte = CRC; // Xmodem 'C' ready to receive;
        const usbService = ServicesRegistry.getService(usbServiceType);
        const allPorts = (await usbService.listPorts(usbSerialPortType)).filter(port => !port.isOpened);
        const results = await Promise.allSettled(allPorts.map(async (port) => {
            const defered = GcPromise.defer();
            const listener = ((details) => {
                if (details.data[0] === heartBeatByte) {
                    defered.resolve(port);
                }
            });
            try {
                port.addEventListener(dataEventType, listener);
                await port.open({ baudRate: baud });
                return await GcPromise.timeout(defered.promise, timeoutMs, 'timeout');
            }
            finally {
                await port.close();
                port.removeEventListener(dataEventType, listener);
            }
        }));
        return results.filter(result => result.status === 'fulfilled')
            .map(result => new XmodemPort(result.value));
    }
}
ServicesRegistry.register(xmodemServiceType, XmodemService);
//  ----------------------  XModem Port  ----------------------
// -----------------------------------------------------------------------------
// Xmodem special characters.
// -----------------------------------------------------------------------------
const SOH = 0x01; // Start of 128B block character.
const STX = 0x02; // Start of 1KB block character.
const EOT = 0x04; // End of text character.
const ACK = 0x06; // Acknowledge character (ACK).
const NAK = 0x15; // Negative acknowledge character (NAK).
const CAN = 0x18; // Cancel character (CAN).
const CRC = 0x43; // CRC character ('C'). Indicates receiver accepts 'CRC' based xmodem protocol.
const BLOCKSIZE = 1024;
const BLOCKSTART = STX;
// -----------------------------------------------------------------------------
// Xmodem Error Messages
// -----------------------------------------------------------------------------
const cannotSend = 'Cannot Send';
const cannotReceive = 'Cannot Receive';
const cannotEOT = 'Cannot Terminate Send';
const badPacket = 'Bad Packet Received';
const errTimeout = 'timeout';
const noCRC = 'No port receiving xmodem commands. Ensure the target device is set to boot from UART, and is reset.';
/**
 * Event type constant for Xmodem progress messages.
 *
 * @const {EventType<IXmodemProgressEvent>}
 */
const xmodemProgressEventType = new EventType('xmodemProgressMessage');
/**
 * Event type constant for Xmodem status messages.
 *
 * @const {EventType<IXmodemStatusEvent>}
 */
const xmodemStatusEventType = new EventType('xmodemStatusMessage');
/**
 * Calculates the CRC16 checksum using the XMODEM algorithm.
 *
 * This function takes an array of numbers as input and returns an array
 * containing the high byte and low byte of the calculated CRC16 checksum.
 *
 * @param   data - The input data to be checksummed.
 * @returns Array containing the high byte and low byte of the
 *                     calculated CRC16 checksum.
 */
function xmodem_crc16(data) {
    const CRC16_XMODEM_TABLE = [
        0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
        0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
        0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
        0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
        0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
        0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
        0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
        0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
        0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
        0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
        0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
        0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
        0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
        0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
        0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
        0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
        0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
        0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
        0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
        0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
        0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
        0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
        0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
        0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
        0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
        0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
        0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
        0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
        0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
        0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
        0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
        0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0,
    ];
    let crc = 0x0000;
    data.forEach(value => {
        crc = ((crc << 8) & 0xff00) ^ CRC16_XMODEM_TABLE[((crc >>> 8) & 0xff) ^ value];
    });
    return [(crc >> 8) & 0xFF, crc & 0xFF]; // High byte, Low Byte
}
/**
 * Default values for an XmodemPort open operation.
 */
const defaultConfigs = {
    filler: 0x1a,
    attemptMaxCrc: 3,
    attemptMaxEot: 2,
    attemptMaxBlock: 5,
    timeoutMs: DEFAULT_TIMEOUT_MS,
    baudRate: DEFAULT_BAUD
};
/**
 * Represents a Xmodem serial port.
 *
 * The XmodemPort class encapsulates a serial port and provides methods to send and receive data using the Xmodem protocol.
 *
 */
class XmodemPort extends Events {
    /**
     * Creates a new instance of XmodemPort.
     *
     * @param port - The serial port to use for communication.
     */
    constructor(port) {
        super();
        this.readBuffer = [];
        this.config = defaultConfigs;
        /**
         * Asynchronous onData receive handler for the Port. It buffers the data
         * received from the port. The read api is used to consume data from the
         * buffer.
         *
         * @param details Data received from Port.
         */
        this.decode = (details) => {
            this.console.logAPI(`${XmodemPort.name}::decode`, this.hexDebug(details.data));
            this.readBuffer = [...this.readBuffer, ...details.data];
            if (this.dataAvail.promise.isPending)
                this.dataAvail.resolve(true);
        };
        this.console = new GcConsole(XMODEM_SERVICE_MODULE_NAME, port.comName);
        this.readBuffer = [];
        this.dataAvail = GcPromise.defer();
        this._port = port;
    }
    /**
     * Open a serial port using Xmodem protocol.
     *
     * This method initializes the connection by identifying the port, opening it with the default baud rate,
     * and setting up event listeners for data receipt.
     */
    async open(params) {
        this.console.logAPI(`${XmodemPort.name}::${this.open.name}`, ...arguments);
        this.config = { ...defaultConfigs, ...params };
        this._port.addEventListener(dataEventType, this.decode);
        await this._port.open({ baudRate: this.config.baudRate });
    }
    /**
     * Close a serial port using Xmodem protocol.
     *
     * This method is responsible for properly terminating the connection with the Xmodem protocol.
     * It closes the port and remove the data event listener attached to it.
     */
    async close() {
        this.console.logAPI(`${XmodemPort.name}::${this.close.name}`, ...arguments);
        if (this._port.isOpened)
            await this._port.close();
        this._port.removeEventListener(dataEventType, this.decode);
    }
    /**
     * Transmits the provided data across the xmodem port.
     *
     * @param data The data to be transmitted.
     */
    encode(data) {
        this.console.logAPI(`${XmodemPort.name}::${this.encode.name}`, this.hexDebug(data));
        this._port.write(data);
    }
    /**
     * Resets the read buffer.
     *
     * This method clears the internal read buffer and resets the data availability promise.
     */
    readBufferReset() {
        this.console.logAPI(`${XmodemPort.name}::${this.readBufferReset.name}`, ...arguments);
        this.readBuffer = [];
        this.dataAvail = GcPromise.defer();
    }
    /**
     * Read xmodem data from the port.
     *
     * @param requestBytes The number of bytes to read. If null,
     *                      will read entirety of buffered data.
     * @returns Promise resolving to an array of bytes read from the xmodem port.
     */
    async read(requestBytes = null) {
        this.console.logAPI(`${XmodemPort.name}::${this.read.name}`, ...arguments);
        let result = [];
        // If specific number of bytes requested
        if (requestBytes) {
            while (this.readBuffer.length < requestBytes) {
                await GcPromise.timeout(this.dataAvail.promise, this.config.timeoutMs, errTimeout);
                this.dataAvail = GcPromise.defer();
            }
            result = this.readBuffer.slice(0, requestBytes);
            this.readBuffer = this.readBuffer.slice(requestBytes, this.readBuffer.length);
            // Otherwise return variable number of bytes
        }
        else {
            if (this.readBuffer.length === 0) {
                await GcPromise.timeout(this.dataAvail.promise, this.config.timeoutMs, errTimeout);
                this.dataAvail = GcPromise.defer();
            }
            result = this.readBuffer;
            this.readBuffer = [];
        }
        return result;
    }
    /**
     * Preview the Xmodem data buffer.
     *
     * This method checks if buffered data is available and returns the first
     * byte in the buffer without modifying the read state.
     * If no data is available, it waits up to {@link #config.timeoutMs}
     * milliseconds for data to become available before timing out.
     *
     * @return The previewed byte value.
     */
    async preview() {
        this.console.logAPI(`${XmodemPort.name}::${this.preview.name}`, ...arguments);
        if (this.readBuffer.length === 0) {
            await GcPromise.timeout(this.dataAvail.promise, this.config.timeoutMs, errTimeout);
        }
        const result = this.readBuffer[0];
        return result;
    }
    /**
     * Sends a sequence of data to the xmodem port.
     *
     * @param data        The data to be sent to the target device.
     * @param dataDescriptor A string describing the data being sent (e.g. " firmware").
     */
    async send(data, dataDescriptor) {
        this.console.logAPI(`${XmodemPort.name}::${this.send.name}`, this.hexDebug(data));
        // --------------------------------------------------------------------
        const getCRC = async (attempt = 1) => {
            try {
                this.readBufferReset();
                const bytes = await this.read();
                if (bytes[0] === CRC)
                    return;
                if (attempt < this.config.attemptMaxCrc)
                    return getCRC(attempt + 1);
            }
            catch (err) {
                throw (new Error(noCRC));
            }
        };
        // --------------------------------------------------------------------
        const sendBlocks = async () => {
            const chunkArray = (arr, chunkSize, padValue) => {
                const result = [];
                for (let i = 0; i < arr.length; i += chunkSize) {
                    const chunk = arr.slice(i, i + chunkSize);
                    while (chunk.length < chunkSize)
                        chunk.push(padValue); // Pad if needed
                    result.push(chunk);
                }
                return result;
            };
            const packets = chunkArray([...data], BLOCKSIZE, this.config.filler);
            const bits8 = (x) => x & 0xff;
            let retrys = 0;
            for (let i = 0; i < packets.length; i++) {
                const blkNum = i + 1;
                const header = [BLOCKSTART, bits8(blkNum), bits8(0xff - blkNum)];
                const block = packets[i];
                const crc = xmodem_crc16(block);
                const datagram = [...header, ...block, ...crc];
                this.encode(datagram);
                this.fireEvent(xmodemProgressEventType, {
                    name: `${dataDescriptor}`,
                    percent: Math.floor((blkNum / packets.length) * 100),
                });
                const response = await this.read();
                switch (response[0]) {
                    case ACK:
                        retrys = 0;
                        break;
                    case CAN: throw (new Error(cannotSend));
                    case NAK: // CRC mismatch, retry
                        if (retrys++ < this.config.attemptMaxBlock) {
                            this.fireEvent(xmodemStatusEventType, {
                                type: 'info',
                                message: `Retry Block ${blkNum} (${retrys}) for ${dataDescriptor}`
                            });
                            i--;
                            break;
                        }
                        else {
                            throw new Error(cannotSend);
                        }
                }
            }
            return true;
        };
        // --------------------------------------------------------------------
        const sendEOT = async (attempt = 1) => {
            this.encode([EOT]);
            switch ((await this.read())[0]) {
                case ACK:
                    this.fireEvent(xmodemStatusEventType, {
                        type: 'info',
                        message: `Load ${dataDescriptor} Complete`
                    });
                    return true;
                case NAK:
                default:
                    if (attempt < this.config.attemptMaxEot)
                        return sendEOT(attempt + 1);
                    else
                        throw (new Error(cannotEOT));
            }
        };
        // --------------------------------------------------------------------
        // Body of send
        // --------------------------------------------------------------------
        await getCRC();
        await sendBlocks();
        await sendEOT();
    }
    /**
     * Receive data from an Xmodem-based transmitter.
     *
     * @return {Promise<number[]>} A promise that resolves to an array of integers
     *                             representing the received data.
     * @throws {Error} If no valid data is received after max attempts.
     */
    async receive() {
        this.console.logAPI(`${XmodemPort.name}::${this.receive.name}`, ...arguments);
        // --------------------------------------------------------------------
        const sendCRC = async (attempt = 1) => {
            this.readBufferReset();
            // Send ready to receive
            this.encode([CRC]);
            try {
                if ([SOH, STX].includes(await this.preview()))
                    return true;
            }
            catch (error) {
                if (error !== errTimeout)
                    throw (error);
            }
            if (attempt < this.config.attemptMaxCrc)
                return sendCRC(attempt + 1);
            else
                throw (new Error(cannotReceive));
        };
        // --------------------------------------------------------------------
        const receiveBlock = async (blkNum, attempt = 1) => {
            // ----------------------------------------------------------------
            const retry = (ackType, errorType) => {
                if (attempt < this.config.attemptMaxBlock) {
                    return receiveBlock(blkNum, attempt + 1);
                }
                else {
                    this.encode([ackType]);
                    throw (new Error(errorType));
                }
            };
            const blockType = await this.preview();
            switch (blockType) {
                case STX:
                case SOH: {
                    const blockSize = blockType === SOH ? 128 : 1024;
                    const datagramSize = 5 + blockSize;
                    const datagram = await this.read(datagramSize);
                    const recBlkNum = datagram[1];
                    const recNegBlkNum = datagram[2];
                    const block = datagram.slice(3, -2);
                    const recCRC = datagram.slice(-2);
                    const calcCRC = xmodem_crc16(block);
                    // Duplicate Block
                    if (recBlkNum === blkNum - 1 && recBlkNum + recNegBlkNum === 0xFF) {
                        return retry(ACK, badPacket);
                    }
                    // Wrong Block Number
                    if (recBlkNum !== blkNum || recBlkNum + recNegBlkNum !== 0xFF) {
                        return retry(NAK, badPacket);
                    }
                    // Incorrect Checksum
                    if (recCRC[0] !== calcCRC[0] || recCRC[1] !== calcCRC[1]) {
                        return retry(NAK, badPacket);
                    }
                    this.encode([ACK]);
                    return block;
                }
                case EOT:
                    await this.read();
                    this.encode([ACK]);
                    return [];
                // MISSING HEADER
                default:
                    await this.read();
                    return retry(NAK, badPacket);
            }
        };
        // --------------------------------------------------------------------
        const removeEndFiller = (arr, filler) => {
            let i = arr.length - 1;
            while (i >= 0 && arr[i] === filler)
                i--;
            return arr.slice(0, i + 1);
        };
        // --------------------------------------------------------------------
        // Body of receive
        // --------------------------------------------------------------------
        let blkNum = 1;
        let result = [];
        let block = [];
        await sendCRC();
        do {
            block = await receiveBlock(blkNum++);
            result = [...result, ...block];
        } while (block.length > 0); // while EOT not received
        return removeEndFiller(result, this.config.filler);
    }
    /**
     * Formats an array of numbers as a hexadecimal string.
     *
     * If the input array has 8 or fewer elements, the entire array is formatted.
     * Otherwise, the first four and last four elements are formatted, with three dots in between to indicate truncation.
     *
     * @param x An array of numbers to be formatted.
     * @return A string representation of the input array as hexadecimal values.
     */
    hexDebug(ary) {
        const hexAry = (x) => JSON.stringify(x.map(num => num.toString(16)));
        if (ary.length <= 8)
            return hexAry(ary);
        return `len:${ary.length} ${hexAry(ary.slice(0, 4))} ... ${hexAry(ary.slice(-4))})`;
    }
    /**
     * Adds an event listener for the specified type of event.
     *
     * @param type The type of event to listen for.
     * @param listener The listener function to invoke when the event is triggered.
     */
    addEventListener(type, listener) {
        this.console.logAPI(`${XmodemPort.name}::${this.addEventListener.name}`, ...arguments);
        super.addEventListener(type, listener);
    }
    /**
     * Removes an event listener previously added using {@link #addEventListener}.
     *
     * @param type The type of event that the listener was listening for.
     * @param listener The listener function to remove.
     */
    removeEventListener(type, listener) {
        this.console.logAPI(`${XmodemPort.name}::${this.removeEventListener.name}`, ...arguments);
        super.removeEventListener(type, listener);
    }
    /**
     * Fires an event of the specified type with the provided details.
     *
     * @param  type    The type of event to fire.
     * @param  details The details of the event.
     */
    fireEvent(type, details) {
        super.fireEvent(type, details);
    }
}

export { ACK, CAN, CRC, DEFAULT_BAUD, DEFAULT_TIMEOUT_MS, EOT, NAK, SOH, STX, XmodemPort, XmodemService, xmodemProgressEventType, xmodemServiceType, xmodemStatusEventType, xmodem_crc16 };
//# sourceMappingURL=XmodemService.js.map
