const jspack = require("jspack").jspack
const Mavlink20 = require("./mavlink20")
const Mavlink20Header = require("./mavlink20_header")
const Mavlink20BadData = require("./mavlink20_bad_data")
const Mavlink20Map = require("./mavlink20_map")

// Buffer web polyfill
class Buffer {
    static concat(uint8ArrayList) {
        return Uint8Array.from(uint8ArrayList.reduce((total, item) => [...total, ...item], []))
    }
    static alloc(length) {
        return new Uint8Array(length).fill(0)
    }
    static from(data) {
        return Uint8Array.from(data)
    }
}

class MAVLink20Processor extends Mavlink20 {
    logger;
    seq = 0;
    buf = [];
    bufInError = [];
    srcSystem = 0;
    srcComponent = 0;

    have_prefix_error = false;
    protocol_marker = 253;
    expected_length = Mavlink20.HEADER_LEN;
    little_endian = true;

    crc_extra = true;
    sort_fields = true;
    total_packets_sent = 0;
    total_bytes_sent = 0;
    total_packets_received = 0;
    total_bytes_received = 0;
    total_receive_errors = 0;
    startup_time = Date.now();

    constructor() {
        super()
    }

    log(level, message) {
        console.log(level, message);
    }

    pushBuffer(data) {
        if (data) {
            this.buf = Buffer.concat([this.buf, data]);
            this.total_bytes_received += data.length;
        }
    }

    parsePrefix() {

        // Test for a message prefix.
        if (this.buf.length >= 1 && this.buf[0] != this.protocol_marker) {

            // Strip the offending initial byte and throw an error.
            var badPrefix = this.buf[0];
            this.bufInError = this.buf.slice(0, 1);
            this.buf = this.buf.slice(1);
            this.expected_length = Mavlink20.HEADER_LEN;

            // TODO: enable subsequent prefix error suppression if robust_parsing is implemented
            //if(!this.have_prefix_error) {
            //    this.have_prefix_error = true;
            throw new Error("Bad prefix (" + badPrefix + ")");
            //}

        }
        //else if( this.buf.length >= 1 && this.buf[0] == this.protocol_marker ) {
        //    this.have_prefix_error = false;
        //}

    }

    parseLength() {

        if (this.buf.length >= 2) {
            var unpacked = jspack.Unpack('BB', this.buf.slice(0, 2));
            this.expected_length = unpacked[1] + Mavlink20.HEADER_LEN + 2 // length of message + header + CRC
        }

    }

    parsePayload() {

        var m = null;

        // If we have enough bytes to try and read it, read it.
        if (this.expected_length >= 8 && this.buf.length >= this.expected_length) {

            // Slice off the expected packet length, reset expectation to be to find a header.
            var mbuf = this.buf.slice(0, this.expected_length);
            // TODO: slicing off the buffer should depend on the error produced by the decode() function
            // - if a message we find a well formed message, cut-off the expected_length
            // - if the message is not well formed (correct prefix by accident), cut-off 1 char only
            this.buf = this.buf.slice(this.expected_length);
            this.expected_length = 6;

            // w.info("Attempting to parse packet, message candidate buffer is ["+mbuf.toByteArray()+"]");

            try {
                m = this.decode(mbuf);
                this.total_packets_received += 1;
            }
            catch (e) {
                // Set buffer in question and re-throw to generic error handling
                this.bufInError = mbuf;
                throw e;
            }
        }

        return m;

    }

    parseChar(c) {

        var m = null;

        try {

            this.pushBuffer(c);
            this.parsePrefix();
            this.parseLength();
            m = this.parsePayload();

        } catch (e) {

            this.log('error', e.message);
            this.total_receive_errors += 1;
            m = new Mavlink20BadData(this.bufInError, e.message);
            this.bufInError = new Buffer.from([]);

        }

        // if (null != m) {
        //     this.emit(m.name, m);
        //     this.emit('message', m);
        //     this.log
        // }

        return m;

    }


    parseBuffer = function (s) {

        // Get a message, if one is available in the stream.
        var m = this.parseChar(s);

        // No messages available, bail.
        if (null === m) {
            return null;
        }

        // While more valid messages can be read from the existing buffer, add
        // them to the array of new messages and return them.
        var ret = [m];
        while (true) {
            m = this.parseChar();
            if (null === m) {
                // No more messages left.
                return ret;
            }
            ret.push(m);
        }

    }

    decode = function (msgbuf) {

        var magic, incompat_flags, compat_flags, mlen, seq, srcSystem, srcComponent, unpacked, msgId;

        // decode the header
        try {
            unpacked = jspack.Unpack('cBBBBBBHB', msgbuf.slice(0, 10));
            magic = unpacked[0];
            mlen = unpacked[1];
            incompat_flags = unpacked[2];
            compat_flags = unpacked[3];
            seq = unpacked[4];
            srcSystem = unpacked[5];
            srcComponent = unpacked[6];
            var msgIDlow = ((unpacked[7] & 0xFF) << 8) | ((unpacked[7] >> 8) & 0xFF);
            var msgIDhigh = unpacked[8];
            msgId = msgIDlow | (msgIDhigh << 16);
        }
        catch (e) {
            throw new Error('Unable to unpack MAVLink header: ' + e.message);
        }

        if (magic.charCodeAt(0) != this.protocol_marker) {
            throw new Error("Invalid MAVLink prefix (" + magic.charCodeAt(0) + ")");
        }

        if (mlen != msgbuf.length - (Mavlink20.HEADER_LEN + 2)) {
            throw new Error("Invalid MAVLink message length.  Got " + (msgbuf.length - (mavlink20.HEADER_LEN + 2)) + " expected " + mlen + ", msgId=" + msgId);
        }

        if (!Mavlink20Map[msgId]) {
            throw new Error("Unknown MAVLink message ID (" + msgId + ")");
        }

        // decode the payload
        // refs: (fmt, type, order_map, crc_extra) = mavlink20.map[msgId]
        var decoder = Mavlink20Map[msgId];

        // decode the checksum
        try {
            var receivedChecksum = jspack.Unpack('<H', msgbuf.slice(msgbuf.length - 2));
        } catch (e) {
            throw new Error("Unable to unpack MAVLink CRC: " + e.message);
        }

        var messageChecksum = Mavlink20.x25Crc(msgbuf.slice(1, msgbuf.length - 2));

        // Assuming using crc_extra = True.  See the message.prototype.pack() function.
        messageChecksum = Mavlink20.x25Crc([decoder.crc_extra], messageChecksum);

        if (receivedChecksum != messageChecksum) {
            throw new Error('invalid MAVLink CRC in msgID ' + msgId + ', got 0x' + receivedChecksum + ' checksum, calculated payload checkum as 0x' + messageChecksum);
        }

        var paylen = jspack.CalcLength(decoder.format);
        var payload = msgbuf.slice(Mavlink20.HEADER_LEN, msgbuf.length - 2);

        //put any truncated 0's back in
        if (paylen > payload.length) {
            payload = Buffer.concat([payload, Buffer.alloc(paylen - payload.length)]);
        }
        // Decode the payload and reorder the fields to match the order map.
        try {
            var t = jspack.Unpack(decoder.format, payload);
        }
        catch (e) {
            throw new Error('Unable to unpack MAVLink payload type=' + decoder.type + ' format=' + decoder.format + ' payloadLength=' + payload + ': ' + e.message);
        }

        // Need to check if the message contains arrays
        var args = {};
        const elementsInMsg = decoder.order_map.length;
        const actualElementsInMsg = JSON.parse(JSON.stringify(t)).length;

        if (elementsInMsg == actualElementsInMsg) {
            // Reorder the fields to match the order map
            // _.each(t, function (e, i, l) {
            //     args[i] = t[decoder.order_map[i]]
            // });
            t.forEach(function (item, i) {
                args[i] = t[decoder.order_map[i]]
            })
        } else {
            // This message contains arrays
            var typeIndex = 1;
            var orderIndex = 0;
            var memberIndex = 0;
            var tempArgs = {};

            // Walk through the fields 
            for (var i = 0, size = decoder.format.length - 1; i <= size; ++i) {
                var order = decoder.order_map[orderIndex];
                var currentType = decoder.format[typeIndex];

                if (isNaN(parseInt(currentType))) {
                    // This field is not an array cehck the type and add it to the args
                    tempArgs[orderIndex] = t[memberIndex];
                    memberIndex++;
                } else {
                    // This field is part of an array, need to find the length of the array
                    var arraySize = ''
                    var newArray = []
                    while (!isNaN(decoder.format[typeIndex])) {
                        arraySize = arraySize + decoder.format[typeIndex];
                        typeIndex++;
                    }

                    // Now that we know how long the array is, create an array with the values
                    for (var j = 0, size = parseInt(arraySize); j < size; ++j) {
                        newArray.push(t[j + orderIndex]);
                        memberIndex++;
                    }

                    // Add the array to the args object
                    arraySize = arraySize + decoder.format[typeIndex];
                    currentType = arraySize;
                    tempArgs[orderIndex] = newArray;
                }
                orderIndex++;
                typeIndex++;
            }

            // Finally reorder the fields to match the order map
            // _.each(t, function (e, i, l) {
            //     args[i] = tempArgs[decoder.order_map[i]]
            // });
            t.forEach(function (item, i) {
                args[i] = tempArgs[decoder.order_map[i]]
            })
        }

        // construct the message object
        try {
            var m = new decoder.type(...(Object.keys(args).map(key => args[key])));
            // m.set.call(m, args);
        }
        catch (e) {
            console.error(e);
            //throw new Error('Unable to instantiate MAVLink message of type ' + decoder.type + ' : ' + e.message);
        }
        m.msgbuf = msgbuf;
        m.payload = payload
        m.crc = receivedChecksum;
        m.header = new Mavlink20Header(msgId, mlen, seq, srcSystem, srcComponent, incompat_flags, compat_flags);
        // this.log(m);
        return m;
    }



}

module.exports = MAVLink20Processor