import GUI from "./gui.js";
import CONFIGURATOR from "./data_storage.js";
import { serial } from "./serial.js";

const MSP = {
    symbols: {
        BEGIN: "$".charCodeAt(0),
        PROTO_V1: "M".charCodeAt(0),
        PROTO_V2: "X".charCodeAt(0),
        FROM_MWC: ">".charCodeAt(0),
        TO_MWC: "<".charCodeAt(0),
        UNSUPPORTED: "!".charCodeAt(0),
        START_OF_TEXT: 0x02,
        END_OF_TEXT: 0x03,
        END_OF_TRANSMISSION: 0x04,
        LINE_FEED: 0x0a,
        CARRIAGE_RETURN: 0x0d,
    },
    constants: {
        PROTOCOL_V1: 1,
        PROTOCOL_V2: 2,
        JUMBO_FRAME_MIN_SIZE: 255,
    },
    decoder_states: {
        IDLE: 0,
        PROTO_IDENTIFIER: 1,
        DIRECTION_V1: 2,
        DIRECTION_V2: 3,
        FLAG_V2: 4,
        PAYLOAD_LENGTH_V1: 5,
        PAYLOAD_LENGTH_JUMBO_LOW: 6,
        PAYLOAD_LENGTH_JUMBO_HIGH: 7,
        PAYLOAD_LENGTH_V2_LOW: 8,
        PAYLOAD_LENGTH_V2_HIGH: 9,
        CODE_V1: 10,
        CODE_JUMBO_V1: 11,
        CODE_V2_LOW: 12,
        CODE_V2_HIGH: 13,
        PAYLOAD_V1: 14,
        PAYLOAD_V2: 15,
        CHECKSUM_V1: 16,
        CHECKSUM_V2: 17,
        CLI_COMMAND: 18,
    },
    state: 0,
    message_direction: 1,
    code: 0,
    dataView: 0,
    message_length_expected: 0,
    message_length_received: 0,
    message_buffer: null,
    message_buffer_uint8_view: null,
    message_checksum: 0,
    crcError: false,

    callbacks: [],
    packet_error: 0,
    unsupported: 0,

    TIMEOUT: 1000,

    last_received_timestamp: null,
    listeners: [],

    cli_buffer: [], // buffer for CLI character output
    cli_output: [],
    cli_callback: null,

    read(readInfo) {
        if (CONFIGURATOR.virtualMode) {
            return;
        }

        const data = new Uint8Array(readInfo.data ?? readInfo);

        for (const chunk of data) {
            switch (this.state) {
                case this.decoder_states.CLI_COMMAND:
                    switch (chunk) {
                        case this.symbols.END_OF_TEXT:
                            this.cli_output.push(this.cli_buffer.join(""));
                            this.cli_buffer.length = 0;
                            if (this.cli_callback) {
                                this.cli_callback(this.cli_output);
                                this.cli_output.length = 0;
                            }
                            this.state = this.decoder_states.IDLE;
                            break;
                        case this.symbols.LINE_FEED:
                            this.cli_output.push(this.cli_buffer.join(""));
                            this.cli_buffer.length = 0;
                            break;
                        case this.symbols.CARRIAGE_RETURN:
                            // ignore CRs
                            break;
                        default:
                            this.cli_buffer.push(String.fromCharCode(chunk));
                            break;
                    }
                    break;
                case this.decoder_states.IDLE: // sync char 1
                    switch (chunk) {
                        case this.symbols.BEGIN:
                            this.state = this.decoder_states.PROTO_IDENTIFIER;
                            break;
                        case this.symbols.START_OF_TEXT:
                            this.state = this.decoder_states.CLI_COMMAND;
                            break;
                    }
                    break;
                case this.decoder_states.PROTO_IDENTIFIER: // sync char 2
                    switch (chunk) {
                        case this.symbols.PROTO_V1:
                            this.state = this.decoder_states.DIRECTION_V1;
                            break;
                        case this.symbols.PROTO_V2:
                            this.state = this.decoder_states.DIRECTION_V2;
                            break;
                        default:
                            console.log(`Unknown protocol char ${String.fromCharCode(chunk)}`);
                            this.state = this.decoder_states.IDLE;
                    }
                    break;
                case this.decoder_states.DIRECTION_V1: // direction (should be >)
                case this.decoder_states.DIRECTION_V2:
                    this.unsupported = 0;
                    switch (chunk) {
                        case this.symbols.FROM_MWC:
                            this.message_direction = 1;
                            break;
                        case this.symbols.TO_MWC:
                            this.message_direction = 0;
                            break;
                        case this.symbols.UNSUPPORTED:
                            this.unsupported = 1;
                            break;
                    }
                    this.state =
                        this.state === this.decoder_states.DIRECTION_V1
                            ? this.decoder_states.PAYLOAD_LENGTH_V1
                            : this.decoder_states.FLAG_V2;
                    break;
                case this.decoder_states.FLAG_V2:
                    // Ignored for now
                    this.state = this.decoder_states.CODE_V2_LOW;
                    break;
                case this.decoder_states.PAYLOAD_LENGTH_V1:
                    this.message_length_expected = chunk;

                    if (this.message_length_expected === this.constants.JUMBO_FRAME_MIN_SIZE) {
                        this.state = this.decoder_states.CODE_JUMBO_V1;
                    } else {
                        this._initialize_read_buffer();
                        this.state = this.decoder_states.CODE_V1;
                    }

                    break;
                case this.decoder_states.PAYLOAD_LENGTH_V2_LOW:
                    this.message_length_expected = chunk;
                    this.state = this.decoder_states.PAYLOAD_LENGTH_V2_HIGH;
                    break;
                case this.decoder_states.PAYLOAD_LENGTH_V2_HIGH:
                    this.message_length_expected |= chunk << 8;
                    this._initialize_read_buffer();
                    this.state =
                        this.message_length_expected > 0
                            ? this.decoder_states.PAYLOAD_V2
                            : this.decoder_states.CHECKSUM_V2;
                    break;
                case this.decoder_states.CODE_V1:
                case this.decoder_states.CODE_JUMBO_V1:
                    this.code = chunk;
                    if (this.message_length_expected > 0) {
                        // process payload
                        if (this.state === this.decoder_states.CODE_JUMBO_V1) {
                            this.state = this.decoder_states.PAYLOAD_LENGTH_JUMBO_LOW;
                        } else {
                            this.state = this.decoder_states.PAYLOAD_V1;
                        }
                    } else {
                        // no payload
                        this.state = this.decoder_states.CHECKSUM_V1;
                    }
                    break;
                case this.decoder_states.CODE_V2_LOW:
                    this.code = chunk;
                    this.state = this.decoder_states.CODE_V2_HIGH;
                    break;
                case this.decoder_states.CODE_V2_HIGH:
                    this.code |= chunk << 8;
                    this.state = this.decoder_states.PAYLOAD_LENGTH_V2_LOW;
                    break;
                case this.decoder_states.PAYLOAD_LENGTH_JUMBO_LOW:
                    this.message_length_expected = chunk;
                    this.state = this.decoder_states.PAYLOAD_LENGTH_JUMBO_HIGH;
                    break;
                case this.decoder_states.PAYLOAD_LENGTH_JUMBO_HIGH:
                    this.message_length_expected |= chunk << 8;
                    this._initialize_read_buffer();
                    this.state = this.decoder_states.PAYLOAD_V1;
                    break;
                case this.decoder_states.PAYLOAD_V1:
                case this.decoder_states.PAYLOAD_V2:
                    this.message_buffer_uint8_view[this.message_length_received] = chunk;
                    this.message_length_received++;

                    if (this.message_length_received >= this.message_length_expected) {
                        this.state =
                            this.state === this.decoder_states.PAYLOAD_V1
                                ? this.decoder_states.CHECKSUM_V1
                                : this.decoder_states.CHECKSUM_V2;
                    }
                    break;
                case this.decoder_states.CHECKSUM_V1:
                    if (this.message_length_expected >= this.constants.JUMBO_FRAME_MIN_SIZE) {
                        this.message_checksum = this.constants.JUMBO_FRAME_MIN_SIZE;
                    } else {
                        this.message_checksum = this.message_length_expected;
                    }
                    this.message_checksum ^= this.code;
                    if (this.message_length_expected >= this.constants.JUMBO_FRAME_MIN_SIZE) {
                        this.message_checksum ^= this.message_length_expected & 0xff;
                        this.message_checksum ^= (this.message_length_expected & 0xff00) >> 8;
                    }
                    for (let ii = 0; ii < this.message_length_received; ii++) {
                        this.message_checksum ^= this.message_buffer_uint8_view[ii];
                    }
                    this._dispatch_message(chunk);
                    break;
                case this.decoder_states.CHECKSUM_V2:
                    this.message_checksum = 0;
                    this.message_checksum = this.crc8_dvb_s2(this.message_checksum, 0); // flag
                    this.message_checksum = this.crc8_dvb_s2(this.message_checksum, this.code & 0xff);
                    this.message_checksum = this.crc8_dvb_s2(this.message_checksum, (this.code & 0xff00) >> 8);
                    this.message_checksum = this.crc8_dvb_s2(
                        this.message_checksum,
                        this.message_length_expected & 0xff,
                    );
                    this.message_checksum = this.crc8_dvb_s2(
                        this.message_checksum,
                        (this.message_length_expected & 0xff00) >> 8,
                    );
                    for (let ii = 0; ii < this.message_length_received; ii++) {
                        this.message_checksum = this.crc8_dvb_s2(
                            this.message_checksum,
                            this.message_buffer_uint8_view[ii],
                        );
                    }
                    this._dispatch_message(chunk);
                    break;
                default:
                    console.log(`Unknown state detected: ${this.state}`);
            }
        }
        this.last_received_timestamp = Date.now();
    },
    _initialize_read_buffer() {
        this.message_buffer = new ArrayBuffer(this.message_length_expected);
        this.message_buffer_uint8_view = new Uint8Array(this.message_buffer);
    },

    _dispatch_message(expectedChecksum) {
        if (this.message_checksum === expectedChecksum) {
            // message received, store dataview
            this.dataView = new DataView(this.message_buffer, 0, this.message_length_expected);
        } else if (serial.protocol === "bluetooth" && serial._protocol?.shouldBypassCrc?.(expectedChecksum)) {
            this.dataView = new DataView(this.message_buffer, 0, this.message_length_expected);
            this.crcError = false; // Override the CRC error for this specific case
        } else {
            this.packet_error++;
            this.crcError = true;
            this.dataView = new DataView(new ArrayBuffer(0));
        }
        this.notify();
        // Reset variables
        this.message_length_received = 0;
        this.state = 0;
        this.crcError = false;
    },
    notify() {
        this.listeners.forEach((listener) => {
            listener(this);
        });
    },
    listen(listener) {
        if (this.listeners.indexOf(listener) === -1) {
            this.listeners.push(listener);
        }
    },
    clearListeners() {
        this.listeners = [];
    },
    crc8_dvb_s2(crc, ch) {
        crc ^= ch;
        for (let ii = 0; ii < 8; ii++) {
            if (crc & 0x80) {
                crc = ((crc << 1) & 0xff) ^ 0xd5;
            } else {
                crc = (crc << 1) & 0xff;
            }
        }
        return crc;
    },
    crc8_dvb_s2_data(data, start, end) {
        let crc = 0;
        for (let ii = start; ii < end; ii++) {
            crc = this.crc8_dvb_s2(crc, data[ii]);
        }
        return crc;
    },
    encode_message_v1(code, data) {
        const dataLength = data ? data.length : 0;
        // always reserve 6 bytes for protocol overhead !
        const bufferSize = dataLength + 6;
        const bufferOut = new ArrayBuffer(bufferSize);
        const bufView = new Uint8Array(bufferOut);

        bufView[0] = 36; // $
        bufView[1] = 77; // M
        bufView[2] = 60; // <
        bufView[3] = dataLength;
        bufView[4] = code;

        let checksum = bufView[3] ^ bufView[4];

        for (let i = 0; i < dataLength; i++) {
            bufView[i + 5] = data[i];
            checksum ^= bufView[i + 5];
        }

        bufView[5 + dataLength] = checksum;
        return bufferOut;
    },
    encode_message_v2(code, data) {
        const dataLength = data ? data.length : 0;
        // 9 bytes for protocol overhead
        const bufferSize = dataLength + 9;
        const bufferOut = new ArrayBuffer(bufferSize);
        const bufView = new Uint8Array(bufferOut);
        bufView[0] = 36; // $
        bufView[1] = 88; // X
        bufView[2] = 60; // <
        bufView[3] = 0; // flag
        bufView[4] = code & 0xff;
        bufView[5] = (code >> 8) & 0xff;
        bufView[6] = dataLength & 0xff;
        bufView[7] = (dataLength >> 8) & 0xff;
        for (let ii = 0; ii < dataLength; ii++) {
            bufView[8 + ii] = data[ii];
        }
        bufView[bufferSize - 1] = this.crc8_dvb_s2_data(bufView, 3, bufferSize - 1);
        return bufferOut;
    },
    encode_message_cli(str) {
        const data = Array.from(str, (c) => c.charCodeAt(0));
        const dataLength = data ? data.length : 0;
        const bufferSize = dataLength + 3; // 3 bytes for protocol overhead
        const bufferOut = new ArrayBuffer(bufferSize);
        const bufView = new Uint8Array(bufferOut);
        bufView[0] = this.symbols.START_OF_TEXT; // STX
        for (let ii = 0; ii < dataLength; ii++) {
            bufView[1 + ii] = data[ii];
        }
        bufView[bufferSize - 2] = this.symbols.LINE_FEED; // LF
        bufView[bufferSize - 1] = this.symbols.END_OF_TEXT; // ETX
        return bufferOut;
    },
    send_cli_command(str, callback) {
        const bufferOut = this.encode_message_cli(str);
        this.cli_callback = callback;

        serial.send(bufferOut);
    },
    send_message(code, data, callback_sent, callback_msp) {
        if (code === undefined || !serial.connected || CONFIGURATOR.virtualMode) {
            if (callback_msp) {
                callback_msp();
            }
            return false;
        }

        const bufferOut = code <= 254 ? this.encode_message_v1(code, data) : this.encode_message_v2(code, data);
        const view = new Uint8Array(bufferOut);
        const keyCrc = this.crc8_dvb_s2_data(view, 0, view.length);
        const requestExists = this.callbacks.some(
            (i) =>
                i.code === code &&
                i.requestBuffer?.byteLength === bufferOut.byteLength &&
                this.crc8_dvb_s2_data(new Uint8Array(i.requestBuffer), 0, i.requestBuffer.byteLength) === keyCrc,
        );

        const obj = {
            code,
            requestBuffer: bufferOut,
            callback: callback_msp,
            start: performance.now(),
        };

        if (!requestExists) {
            obj.timer = setTimeout(() => {
                console.warn(
                    `MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,
                );
                serial.send(bufferOut, (_sendInfo) => {
                    obj.stop = performance.now();
                    const executionTime = Math.round(obj.stop - obj.start);
                    // We should probably give up connection if the request takes too long ?
                    if (executionTime > 5000) {
                        console.warn(
                            `MSP: data request took too long: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} EXECUTION TIME: ${executionTime}ms`,
                        );
                    }

                    clearTimeout(obj.timer); // prevent leaks
                });
            }, this.TIMEOUT);
        }

        this.callbacks.push(obj);

        // always send messages with data payload (even when there is a message already in the queue)
        if (data || !requestExists) {
            serial.send(bufferOut, (sendInfo) => {
                if (sendInfo.bytesSent === bufferOut.byteLength && callback_sent) {
                    callback_sent();
                }
            });
        }

        return true;
    },
    /**
     * resolves: {command: code, data: data, length: message_length}
     */
    async promise(code, data) {
        return new Promise((resolve) => {
            this.send_message(code, data, false, (_data) => {
                resolve(_data);
            });
        });
    },
    callbacks_cleanup() {
        for (const callback of this.callbacks) {
            clearTimeout(callback.timer);
        }

        this.callbacks = [];
    },
    disconnect_cleanup() {
        this.state = 0; // reset packet state for "clean" initial entry (this is only required if user hot-disconnects)
        this.packet_error = 0; // reset CRC packet error counter for next session

        this.callbacks_cleanup();
    },
};

MSP.SDCARD_STATE_NOT_PRESENT = 0;
MSP.SDCARD_STATE_FATAL = 1;
MSP.SDCARD_STATE_CARD_INIT = 2;
MSP.SDCARD_STATE_FS_INIT = 3;
MSP.SDCARD_STATE_READY = 4;

window.MSP = MSP;
export default MSP;
