#!/usr/bin/env python3
'''
parse a MAVLink protocol XML file and generate a Node.js javascript module implementation

Based on original work Copyright Andrew Tridgell 2011
Released under GNU GPL version 3 or later
'''

import os
import textwrap
from . import mavtemplate
import sys

t = mavtemplate.MAVTemplate()


def get_mavhead(xml):
    return ("mavlink20" if xml.protocol_marker == 253 else "mavlink10")

def get_mavprocessor(xml):
    return ("MAVLink20Processor" if xml.protocol_marker == 253 else "MAVLink10Processor")

def generate_preamble(outf, msgs, args, xml):
    print("Generating preamble")
    t.write(outf, """
/*
MAVLink protocol implementation for node.js (auto-generated by mavgen_javascript.py)

Generated from: ${FILELIST}

Note: this file has been auto-generated. DO NOT EDIT
*/

// Detect environment
const isNode = typeof process !== 'undefined' &&
               process.versions &&
               process.versions.node;

// Handle jspack dependency
let jspack;
if (isNode) {
    jspack = (global && global.jspack) || require("jspack").jspack;
} else {
    import("./local_modules/jspack/jspack.js").then((mod) => {
	jspack = new mod.default()
    }).catch((e) => {
    });
}

// Handle Node.js specific modules
let events, util;
if (isNode) {
    events = require("events");
    util = require("util");
} else {
    // Browser polyfills for Node.js modules
    util = { 
        inherits: function(constructor, superConstructor) {
            constructor.prototype = Object.create(superConstructor.prototype);
            constructor.prototype.constructor = constructor;
        }
    };
    
    // Simple EventEmitter polyfill for browsers
    events = { 
        EventEmitter: function() {
            this._events = {};
            
            this.on = function(event, listener) {
                if (!this._events[event]) {
                    this._events[event] = [];
                }
                this._events[event].push(listener);
            };
            
            this.emit = function(event, ...args) {
                if (this._events[event]) {
                    this._events[event].forEach(listener => {
                        try {
                            listener.apply(this, args);
                        } catch (e) {
                            console.error('Error in event listener:', e);
                        }
                    });
                }
            };
            
            this.removeListener = function(event, listener) {
                if (this._events[event]) {
                    const index = this._events[event].indexOf(listener);
                    if (index > -1) {
                        this._events[event].splice(index, 1);
                    }
                }
            };
        }
    };
}


${MAVHEAD} = function(){};

// Implement the CRC-16/MCRF4XX function (present in the Python version through the mavutil.py package)
${MAVHEAD}.x25Crc = function(buffer, crcIN) {

    var bytes = buffer;
    var crcOUT = crcIN ===  undefined ? 0xffff : crcIN;
    bytes.forEach(function(e) {
        var tmp = e ^ (crcOUT & 0xff);
        tmp = (tmp ^ (tmp << 4)) & 0xff;
        crcOUT = (crcOUT >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4);
        crcOUT = crcOUT & 0xffff;
    });
    return crcOUT;

}

${MAVHEAD}.WIRE_PROTOCOL_VERSION = "${WIRE_PROTOCOL_VERSION}";
${MAVHEAD}.PROTOCOL_MARKER_V1 = 0xFE 
${MAVHEAD}.PROTOCOL_MARKER_V2 = 0xFD 
${MAVHEAD}.HEADER_LEN_V1 = 6 
${MAVHEAD}.HEADER_LEN_V2 = 10 
${MAVHEAD}.HEADER_LEN = ${HEADERLEN};

${MAVHEAD}.MAVLINK_TYPE_CHAR     = 0
${MAVHEAD}.MAVLINK_TYPE_UINT8_T  = 1
${MAVHEAD}.MAVLINK_TYPE_INT8_T   = 2
${MAVHEAD}.MAVLINK_TYPE_UINT16_T = 3
${MAVHEAD}.MAVLINK_TYPE_INT16_T  = 4
${MAVHEAD}.MAVLINK_TYPE_UINT32_T = 5
${MAVHEAD}.MAVLINK_TYPE_INT32_T  = 6
${MAVHEAD}.MAVLINK_TYPE_UINT64_T = 7
${MAVHEAD}.MAVLINK_TYPE_INT64_T  = 8
${MAVHEAD}.MAVLINK_TYPE_FLOAT    = 9
${MAVHEAD}.MAVLINK_TYPE_DOUBLE   = 10

${MAVHEAD}.MAVLINK_IFLAG_SIGNED = 0x01
${MAVHEAD}.MAVLINK_SIGNATURE_BLOCK_LEN = 13

// Mavlink headers incorporate sequence, source system (platform) and source component. 
${MAVHEAD}.header = function(msgId, mlen, seq, srcSystem, srcComponent, incompat_flags=0, compat_flags=0) {

    this.mlen = ( typeof mlen === 'undefined' ) ? 0 : mlen;
    this.seq = ( typeof seq === 'undefined' ) ? 0 : seq;
    this.srcSystem = ( typeof srcSystem === 'undefined' ) ? 0 : srcSystem;
    this.srcComponent = ( typeof srcComponent === 'undefined' ) ? 0 : srcComponent;
    this.msgId = msgId
    this.incompat_flags = incompat_flags
    this.compat_flags = compat_flags

}
""", {'FILELIST' : ",".join(args),
      'PROTOCOL_MARKER' : xml.protocol_marker,
      'crc_extra' : xml.crc_extra,
      'WIRE_PROTOCOL_VERSION' : ("2.0" if xml.protocol_marker == 253 else "1.0"),
      'MAVHEAD': get_mavhead(xml),
      'HEADERLEN': ("10" if xml.protocol_marker == 253 else "6")})

    # Mavlink2
    if (xml.protocol_marker == 253):
        t.write(outf, """
${MAVHEAD}.header.prototype.pack = function() {
    return jspack.Pack('BBBBBBBHB', [${PROTOCOL_MARKER}, this.mlen, this.incompat_flags, this.compat_flags, this.seq, this.srcSystem, this.srcComponent, ((this.msgId & 0xFF) << 8) | ((this.msgId >> 8) & 0xFF), this.msgId>>16]);
}
""", {'PROTOCOL_MARKER' : xml.protocol_marker,
              'MAVHEAD': get_mavhead(xml)})
    # Mavlink1
    else:
        t.write(outf, """

${MAVHEAD}.header.prototype.pack = function() {
    return jspack.Pack('BBBBBB', [${PROTOCOL_MARKER}, this.mlen, this.seq, this.srcSystem, this.srcComponent, this.msgId]);
}
""", {'PROTOCOL_MARKER' : xml.protocol_marker,
              'MAVHEAD': get_mavhead(xml)})

    t.write(outf, """

// Base class declaration: mavlink.message will be the parent class for each
// concrete implementation in mavlink.messages.
${MAVHEAD}.message = function() {};

// Convenience setter to facilitate turning the unpacked array of data into member properties
${MAVHEAD}.message.prototype.set = function(args,verbose) {
    // inspect
    this.fieldnames.forEach(function(e, i) {
        var num = parseInt(i,10);
        if (this.hasOwnProperty(e) && isNaN(num)  ){ // asking for an attribute that's non-numeric is ok unless its already an attribute we have
            if ( verbose >= 1) { console.log("WARNING, overwriting an existing property is DANGEROUS:"+e+" ==>"+i+"==>"+args[i]+" -> "+JSON.stringify(this)); }
        }
    }, this);

    // then modify
    this.fieldnames.forEach(function(e, i) {
        this[e] = args[i];
    }, this);
};

/*
  sha256 implementation
  embedded to avoid async issues in web browsers with crypto library
  with thanks to https://geraintluff.github.io/sha256/
*/
${MAVHEAD}.sha256 = function(inputBytes) {
    const K = new Uint32Array([
        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b,
        0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01,
        0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7,
        0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
        0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152,
        0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
        0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
        0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
        0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
        0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08,
        0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f,
        0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
        0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
    ]);

    function ROTR(n, x) { return (x >>> n) | (x << (32 - n)); }

    function Σ0(x) { return ROTR(2, x) ^ ROTR(13, x) ^ ROTR(22, x); }
    function Σ1(x) { return ROTR(6, x) ^ ROTR(11, x) ^ ROTR(25, x); }
    function σ0(x) { return ROTR(7, x) ^ ROTR(18, x) ^ (x >>> 3); }
    function σ1(x) { return ROTR(17, x) ^ ROTR(19, x) ^ (x >>> 10); }

    function Ch(x, y, z) { return (x & y) ^ (~x & z); }
    function Maj(x, y, z) { return (x & y) ^ (x & z) ^ (y & z); }

    const H = new Uint32Array([
        0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
        0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
    ]);

    const l = inputBytes.length;
    const bitLen = l * 8;

    const withPadding = new Uint8Array(((l + 9 + 63) >> 6) << 6); // pad to multiple of 64 bytes
    withPadding.set(inputBytes);
    withPadding[l] = 0x80;
    withPadding.set([
        0, 0, 0, 0,
        (bitLen >>> 24) & 0xff,
        (bitLen >>> 16) & 0xff,
        (bitLen >>> 8) & 0xff,
        bitLen & 0xff
    ], withPadding.length - 8);

    const w = new Uint32Array(64);
    for (let i = 0; i < withPadding.length; i += 64) {
        for (let j = 0; j < 16; j++) {
            w[j] = (
                (withPadding[i + 4 * j] << 24) |
                (withPadding[i + 4 * j + 1] << 16) |
                (withPadding[i + 4 * j + 2] << 8) |
                (withPadding[i + 4 * j + 3])
            ) >>> 0;
        }
        for (let j = 16; j < 64; j++) {
            w[j] = (σ1(w[j - 2]) + w[j - 7] + σ0(w[j - 15]) + w[j - 16]) >>> 0;
        }

        let [a, b, c, d, e, f, g, h] = H;

        for (let j = 0; j < 64; j++) {
            const T1 = (h + Σ1(e) + Ch(e, f, g) + K[j] + w[j]) >>> 0;
            const T2 = (Σ0(a) + Maj(a, b, c)) >>> 0;
            h = g;
            g = f;
            f = e;
            e = (d + T1) >>> 0;
            d = c;
            c = b;
            b = a;
            a = (T1 + T2) >>> 0;
        }

        H[0] = (H[0] + a) >>> 0;
        H[1] = (H[1] + b) >>> 0;
        H[2] = (H[2] + c) >>> 0;
        H[3] = (H[3] + d) >>> 0;
        H[4] = (H[4] + e) >>> 0;
        H[5] = (H[5] + f) >>> 0;
        H[6] = (H[6] + g) >>> 0;
        H[7] = (H[7] + h) >>> 0;
    }

    const output = new Uint8Array(32);
    for (let i = 0; i < 8; i++) {
        output[i * 4 + 0] = (H[i] >>> 24) & 0xff;
        output[i * 4 + 1] = (H[i] >>> 16) & 0xff;
        output[i * 4 + 2] = (H[i] >>> 8) & 0xff;
        output[i * 4 + 3] = H[i] & 0xff;
    }

    return output;
}

// create a message signature
${MAVHEAD}.create_signature = function(key, msgbuf) {
    const input = new Uint8Array(32 + msgbuf.length);
    input.set(key, 0);
    input.set(msgbuf, 32);

    const hash = mavlink20.sha256(input);
    const sig = hash.slice(0, 6);

    return sig;
}

// sign outgoing packet
${MAVHEAD}.message.prototype.sign_packet = function( mav) {
    function packUint48LE(value) {
        const bytes = []
        for (let i = 0; i < 6; i++) {
            bytes.push(Number((value >> BigInt(8 * i)) & 0xFFn));
        }
        return bytes;
    }

    var tsbuf = packUint48LE(BigInt(mav.signing.timestamp));

    // first add the linkid(1 byte) and timestamp(6 bytes) that start the signature
    this._msgbuf = this._msgbuf.concat([mav.signing.link_id])
    this._msgbuf = this._msgbuf.concat(tsbuf);

    sig = mavlink20.create_signature(mav.signing.secret_key, this._msgbuf);
    this._msgbuf = this._msgbuf.concat( ... sig );

    mav.signing.timestamp += 1;
}

// This pack function builds the header and produces a complete MAVLink message,
// including header and message CRC.
${MAVHEAD}.message.prototype.pack = function(mav, crc_extra, payload) {

    this._payload = payload;
    var plen = this._payload.length;
""", {'MAVHEAD': get_mavhead(xml)})

    # Mavlink2 only
    if (xml.protocol_marker == 253):
        t.write(outf, """
    //in MAVLink2 we can strip trailing zeros off payloads. This allows for simple
    // variable length arrays and smaller packets
    while ((plen > 1) && ( (this._payload[plen-1] == 0) || (this._payload[plen-1] == null) ) ) {
            plen = plen - 1;
    }
    this._payload = this._payload.slice(0, plen);
    """)

    t.write(outf, """
// signing is our first incompat flag. 
    var incompat_flags = 0; 
    if (mav.signing.sign_outgoing){ 
            incompat_flags |= ${MAVHEAD}.MAVLINK_IFLAG_SIGNED 
    } 
    // header 
    this._header = new ${MAVHEAD}.header(this._id, this._payload.length, mav.seq, mav.srcSystem, mav.srcComponent, incompat_flags, 0,);
    // payload     
    this._msgbuf = this._header.pack().concat(this._payload);
    // crc -  for now, assume always using crc_extra = True.  TODO: check/fix this. 
    var crc = ${MAVHEAD}.x25Crc(this._msgbuf.slice(1));
    crc = ${MAVHEAD}.x25Crc([crc_extra], crc);
    this._msgbuf = this._msgbuf.concat(jspack.Pack('<H', [crc] ) );

    // signing 
    this._signed     = false 
    this._link_id    = undefined 

    //console.log(mav.signing); 
    //optionally add signing 
    if (mav.signing.sign_outgoing){ 
                this.sign_packet(mav) 
    } 
    return this._msgbuf;

}

""", {'MAVHEAD': get_mavhead(xml)})

def generate_enums(outf, enums, xml):
    print("Generating enums")
    outf.write("\n// enums\n")
    wrapper = textwrap.TextWrapper(initial_indent="", subsequent_indent="                        // ")
    for e in enums:
        outf.write("\n// %s\n" % e.name)
        for entry in e.entry:
            t.write(outf, "${MAVHEAD}.${ENUMNAME} = ${ENUMVAL} // ${ENUMDESC}\n", {'ENUMNAME': entry.name,
                                                                                'ENUMVAL': entry.value,
                                                                                'ENUMDESC': wrapper.fill(entry.description),
                                                                                'MAVHEAD': get_mavhead(xml)})

def generate_message_ids(outf, msgs, xml):
    print("Generating message IDs")
    outf.write("\n// message IDs\n")
    t.write(outf, "${MAVHEAD}.MAVLINK_MSG_ID_BAD_DATA = -1\n", {'MAVHEAD': get_mavhead(xml)})
    for m in msgs:
        t.write(outf, "${MAVHEAD}.MAVLINK_MSG_ID_${MNAME} = ${MVAL}\n", {'MAVHEAD': get_mavhead(xml),
                                                                      'MNAME': m.name.upper(),
                                                                      'MVAL': m.id})

def generate_classes(outf, msgs, xml):
    """
    Generate the implementations of the classes representing MAVLink messages.

    """
    print("Generating class definitions")
    wrapper = textwrap.TextWrapper(initial_indent="", subsequent_indent="")
    t.write(outf, "\n${MAVHEAD}.messages = {};\n\n", {'MAVHEAD': get_mavhead(xml)})

    def field_descriptions(fields):
        ret = ""
        for f in fields:
            if not f.omit_arg:
                ret += "                %-18s        : %s (%s)\n" % (f.name, f.description.strip(), f.type)
        return ret

    # now do all the messages
    for m in msgs:

        # assemble some strings we'll use later in outputting ..
        comment = "%s\n\n%s" % (wrapper.fill(m.description.strip()), field_descriptions(m.fields))
        argfieldnames = []
        conststr = ""
        for f in m.fields:
            if not f.omit_arg:
                argfieldnames.append(f.name)
            else:
                conststr = conststr + "    this.%s = %s;\n" % (f.name, f.const_value)

        # instance field support copied from mavgen_python
        if m.instance_field is not None:
            instance_field = "'%s'" % m.instance_field
            instance_offset = m.field_offsets[m.instance_field]
        else:
            instance_field = "undefined"
            instance_offset = -1

        # start with the comment block
        outf.write("""
/* 
%s
*/
""" % (comment))

        # function signature + declaration
        outf.write("    %s.messages.%s = function(" % ( get_mavhead(xml), m.name.lower() ) )
        outf.write(" ...moreargs ) {\n")
        # passing the dynamic args into the correct attributes, we can call the constructor with or without the 'moreargs'
        outf.write("    [ this.%s ] = moreargs;\n" % " , this.".join(argfieldnames))
        outf.write(conststr)

        # body: set message type properties    
        outf.write("""
    this._format = '%s';
    this._id = %s.MAVLINK_MSG_ID_%s;
    this.order_map = %s;
    this.len_map = %s;
    this.array_len_map = %s;
    this.crc_extra = %u;
    this._name = '%s';

    this._instance_field = %s;
    this._instance_offset = %d;

"""     % (
        m.fmtstr, 
        get_mavhead(xml), 
        m.name.upper(), 
        m.order_map, 
        m.len_map,  
        m.array_len_map, 
        m.crc_extra, 
        m.name.upper(),
        instance_field,
        instance_offset
        ))
        
        # body: set own properties
        if len(m.fieldnames) != 0:
                outf.write("    this.fieldnames = ['%s'];\n" % "', '".join(m.fieldnames))

        outf.write("\n}\n")

        # inherit methods from the base message class
        outf.write("\n%s.messages.%s.prototype = new %s.message;\n" % ( get_mavhead(xml), m.name.lower() ,get_mavhead(xml) ) )

        orderedfields =    "var orderedfields = [ this." + ", this.".join(m.ordered_fieldnames) + "];";


        # Implement the pack() function for this message
        t.write(outf, """
${MAVHEAD}.messages.${MNAME}.prototype.pack = function(mav) {
    ${MORDERED}
    var j = jspack.Pack(this._format, orderedfields);
    if (j === false ) throw new Error("jspack unable to handle this packet");
    return ${MAVHEAD}.message.prototype.pack.call(this, mav, this.crc_extra, j );\n}\n\n""", {'MORDERED': orderedfields, 'MAVHEAD': get_mavhead(xml), 'MNAME': m.name.lower()})

  

def mavfmt(field):
    '''work out the struct format for a type'''
    map = {
        'float'    : 'f',
        'double'   : 'd',
        'char'     : 'c',
        'int8_t'   : 'b',
        'uint8_t'  : 'B',
        'uint8_t_mavlink_version'  : 'B',
        'int16_t'  : 'h',
        'uint16_t' : 'H',
        'int32_t'  : 'i',
        'uint32_t' : 'I',
        'int64_t'  : 'q',
        'uint64_t' : 'Q',
        }

    if field.array_length:
        if field.type in ['char', 'int8_t', 'uint8_t']:
            return str(field.array_length)+'s'
        return str(field.array_length)+map[field.type]
    return map[field.type]

def generate_mavlink_class(outf, msgs, xml):
    print("Generating MAVLink class")

    # Write mapper to enable decoding based on the integer message type
    t.write(outf, "\n\n${MAVHEAD}.map = {\n", {'MAVHEAD': get_mavhead(xml)});
    for m in msgs:
        outf.write("        %s: { format: '%s', type: %s.messages.%s, order_map: %s, crc_extra: %u },\n" % (
            m.id, m.fmtstr, get_mavhead(xml), m.name.lower(), m.order_map, m.crc_extra))
    outf.write("}\n\n")
    
    t.write(outf, """

// Special mavlink message to capture malformed data packets for debugging
${MAVHEAD}.messages.bad_data = function(data, reason) {
    this._id = ${MAVHEAD}.MAVLINK_MSG_ID_BAD_DATA;
    this._data = data;
    this._reason = reason;
    this._msgbuf = data;
}
${MAVHEAD}.messages.bad_data.prototype = new ${MAVHEAD}.message;

//  MAVLink signing state class
MAVLinkSigning = function MAVLinkSigning(object){
        this.secret_key = new Uint8Array();
        this.timestamp = 1;
        this.link_id = 0;
        this.sign_outgoing = false; // todo false this
        this.allow_unsigned_callback = undefined;
        this.stream_timestamps = {};
        this.sig_count = 0;
        this.badsig_count = 0;
        this.goodsig_count = 0;
        this.unsigned_count = 0;
        this.reject_count = 0;
}

/* MAVLink protocol handling class */
${MAVPROCESSOR} = function(logger, srcSystem, srcComponent) {

    this.logger = logger;

    this.seq = 0;
    this.buf = new Uint8Array();
    this.bufInError = new Uint8Array();

    this.srcSystem = (typeof srcSystem === 'undefined') ? 0 : srcSystem;
    this.srcComponent =  (typeof srcComponent === 'undefined') ? 0 : srcComponent;

    this.have_prefix_error = false;

    // The first packet we expect is a valid header, 6 bytes.
    this.protocol_marker = ${PROTOCOL_MARKER};
    this.expected_length = ${MAVHEAD}.HEADER_LEN;
    this.little_endian = true;

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

    // optional , but when used we store signing state in this object:
    this.signing = new MAVLinkSigning();
}

if (!isNode) {
    // Browser-compatible util.inherits replacement
    ${MAVPROCESSOR}.prototype = Object.create(events.EventEmitter.prototype);
    ${MAVPROCESSOR}.prototype.constructor = ${MAVPROCESSOR};
} else {
    // Implements EventEmitter
    util.inherits(${MAVPROCESSOR}, events.EventEmitter);
}

// If the logger exists, this function will add a message to it.
// Assumes the logger is a winston object.
${MAVPROCESSOR}.prototype.log = function(message) {
    if(this.logger) {
        this.logger.info(message);
    }
}

${MAVPROCESSOR}.prototype.log = function(level, message) {
    if(this.logger) {
        this.logger.log(level, message);
    }
}

${MAVPROCESSOR}.prototype.send = function(mavmsg) {
    buf = mavmsg.pack(this);
    this.file.write(buf);
    this.seq = (this.seq + 1) % 256;
    this.total_packets_sent +=1;
    this.total_bytes_sent += buf.length;
}

// return number of bytes needed for next parsing stage
${MAVPROCESSOR}.prototype.bytes_needed = function() {
    ret = this.expected_length - this.buf.length;
    return ( ret <= 0 ) ? 1 : ret;
}

// Combine two buffers into one
${MAVPROCESSOR}.prototype.concat_buffer = function(A, B) {
    const out = new Uint8Array(A.length + B.length)
    out.set(A, 0)
    out.set(B, A.length)
    return out
}

// add data to the local buffer
${MAVPROCESSOR}.prototype.pushBuffer = function(data) {
    if (typeof data.length === 'undefined') {
       data = [data];
    }
    this.buf = this.concat_buffer(this.buf, data);
    this.total_bytes_received += data.length;
}

// Decode prefix.  Elides the prefix.
${MAVPROCESSOR}.prototype.parsePrefix = function() {

    // Test for a message prefix.
    if( this.buf.length >= 1 &&
        this.buf[0] != ${MAVHEAD}.PROTOCOL_MARKER_V2 &&
        this.buf[0] != ${MAVHEAD}.PROTOCOL_MARKER_V1) {

        // Strip the offending initial bytes and throw an error.
        var badPrefix = this.buf[0];
        var idx1 = this.buf.indexOf(${MAVHEAD}.PROTOCOL_MARKER_V1);
        var idx2 = this.buf.indexOf(${MAVHEAD}.PROTOCOL_MARKER_V2);
        if (idx1 == -1) {
            idx1 = idx2;
        }
        if (idx1 == -1 && idx2 == -1) {
            this.bufInError = this.buf;
            this.buf = new Uint8Array();
        } else {
            this.bufInError = this.buf.slice(0,idx1);
            this.buf = this.buf.slice(idx1);
        }
        this.expected_length = ${MAVHEAD}.HEADER_LEN; //initially we 'expect' at least the length of the header, later parseLength corrects for this. 
        throw new Error("Bad prefix ("+badPrefix+")");
    }

}

// Determine the length.  Leaves buffer untouched.
// Although the 'len' of a packet is available as of the second byte, the third byte with 'incompat_flags' lets
//  us know if we have signing enabled, which affects the real-world length by the signature-block length of 13 bytes.
// once successful, 'this.expected_length' is correctly set for the whole packet.
${MAVPROCESSOR}.prototype.parseLength = function() {

    if( this.buf.length >= 3 ) {
        var unpacked = jspack.Unpack('BBB', this.buf.slice(0, 3));
        var magic = unpacked[0]; // stx ie fd or fe etc
        this.expected_length = unpacked[1] + ${MAVHEAD}.HEADER_LEN + 2 // length of message + header + CRC (ie non-signed length)
        this.incompat_flags = unpacked[2];
        // mavlink2 only..  in mavlink1, incompat_flags var above is actually the 'seq', but for this test its ok.
        if ((magic == ${MAVHEAD}.PROTOCOL_MARKER_V2 ) && ( this.incompat_flags & ${MAVHEAD}.MAVLINK_IFLAG_SIGNED )){
            this.expected_length += ${MAVHEAD}.MAVLINK_SIGNATURE_BLOCK_LEN;
        }
    }

}

// input some data bytes, possibly returning a new message - python equiv function is called parse_char / __parse_char_legacy
// c can be null to process any remaining data in the input buffer from a previous call
${MAVPROCESSOR}.prototype.parseChar = function(c) {

    var m = null;

    try {
        if (c != null) {
            this.pushBuffer(c);
        }
        this.parsePrefix();
        this.parseLength();
        m = this.parsePayload();

    } catch(e) {
        this.log('error', e.message);
        this.total_receive_errors += 1;
        m = new ${MAVHEAD}.messages.bad_data(this.bufInError, e.message);
        this.bufInError = new Uint8Array();

    }

    // emit a packet-specific message as well as a generic message, user/s can choose to use either or both of these.
    if (isNode && null != m) {
        this.emit(m._name, m);
        this.emit('message', m);
    }

    return m;

}

// continuation of python's  __parse_char_legacy 
${MAVPROCESSOR}.prototype.parsePayload = function() {

    var m = null;

    // tip: this.expected_length and this.incompat_flags both already set correctly by parseLength(..) above 

    // If we have enough bytes to try and read it, read it.  
    //  shortest packet is header+checksum(2) with no payload, so we need at least that many 
    //  but once we have a longer 'expected length' we have to read all of it. 
    if(( this.expected_length >= ${MAVHEAD}.HEADER_LEN+2) && (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 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 = ${MAVHEAD}.HEADER_LEN; // after attempting a parse, we'll next expect to find just a header. 

        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;

}

// input some data bytes, possibly returning an array of new messages
${MAVPROCESSOR}.prototype.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 = [];
    ret.push(m);
    while(true) {
        m = this.parseChar(null);
        if ( null === m ) {
            // No more messages left.
            return ret;
        }
        ret.push(m);
    }

}

//check signature on incoming message , many of the comments in this file come from the python impl
${MAVPROCESSOR}.prototype.check_signature = function(msgbuf, srcSystem, srcComponent) {
    var timestamp_buf = msgbuf.slice(-12,-6);
 
    var link_id;
    if (isNode) {
        var link_id_buf = Buffer.from ? Buffer.from(msgbuf.slice(-13,-12)) : new Buffer(msgbuf.slice(-13,-12));
        link_id = link_id_buf[0]; // get the first byte.
    } else {
        // Browser-compatible buffer handling
        link_id = msgbuf.slice(-13,-12)[0];
    }

    function unpackUint48LE(bytes) {
        let value = 0n;
        for (let i = 5; i >= 0; i--) {
            value = (value << 8n) | BigInt(bytes[i]);
        }
        return value;
    }
    var timestamp = Number(unpackUint48LE(timestamp_buf));

    // see if the timestamp is acceptable

    // we'll use a STRING containing these three things in it as a unique key eg: '0,1,1'
    stream_key = new Array(link_id,srcSystem,srcComponent).toString();

    if (stream_key in this.signing.stream_timestamps){
	if (timestamp <= this.signing.stream_timestamps[stream_key]){
	    //# reject old timestamp
	    //console.log('old timestamp')
	    return false
	}
    }else{
	//# a new stream has appeared. Accept the timestamp if it is at most
	//# one minute behind our current timestamp
    if (timestamp + 6000*1000 < this.signing.timestamp){
	    //console.log('bad new stream ', timestamp/(100.0*1000*60*60*24*365), this.signing.timestamp/(100.0*1000*60*60*24*365))
	    return false
	}
	this.signing.stream_timestamps[stream_key] = timestamp;
	//console.log('new stream',this.signing.stream_timestamps)
    }

    // just the last 6 of 13 available are the actual sig . ie excluding the linkid(1) and timestamp(6)
    var sigpart = msgbuf.slice(-6);
    sigpart = Uint8Array.from(sigpart);
    // not sig part 0- end-minus-6
    var notsigpart = msgbuf.slice(0,-6);
    notsigpart = Uint8Array.from(notsigpart);

    var sig1 = mavlink20.create_signature(this.signing.secret_key, notsigpart);

    // Browser-compatible buffer comparison
    var signaturesMatch;
    if (isNode) {
        signaturesMatch = Buffer.from(sig1).equals(Buffer.from(sigpart));
    } else {
        // Compare arrays element by element in browser
        signaturesMatch = sig1.length === sigpart.length && 
                         sig1.every((val, index) => val === sigpart[index]);
    }
    if (!signaturesMatch) {
        return false;
    }
    //# the timestamp we next send with is the max of the received timestamp and
    //# our current timestamp
    this.signing.timestamp = Math.max(this.signing.timestamp, timestamp+1);
    return true
}

/* decode a buffer as a MAVLink message */
${MAVPROCESSOR}.prototype.decode = function(msgbuf) {

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

    // decode the header
    try {
        """, {'MAVPROCESSOR': get_mavprocessor(xml),
              'MAVHEAD': get_mavhead(xml),
              'PROTOCOL_MARKER': xml.protocol_marker})
    # Mavlink2 or mavlink1 only
    if (xml.protocol_marker == 253):
        t.write(outf, """
if (msgbuf[0] == 253) {
    var unpacked = jspack.Unpack('BBBBBBBHB', msgbuf.slice(0, 10));  // the H in here causes msgIDlow to takeup 2 bytes, the rest 1
        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); // first-two msgid bytes 
        var msgIDhigh = unpacked[8];   // the 3rd msgid byte 
        msgId = msgIDlow | (msgIDhigh<<16);  // combined result. 0 - 16777215  24bit number
        header_len = 10;
} else {
    var unpacked = jspack.Unpack('BBBBBB', msgbuf.slice(0, 6));
        magic = unpacked[0];
        mlen = unpacked[1];
        seq = unpacked[2];
        srcSystem = unpacked[3];
        srcComponent = unpacked[4];
        msgID = unpacked[5];
        incompat_flags = 0;
        compat_flags = 0;
        header_len = 6;
}
        """)
    # Mavlink1 only
    else:
        t.write(outf, """
var unpacked = jspack.Unpack('BBBBBB', msgbuf.slice(0, 6));
        magic = unpacked[0];
        mlen = unpacked[1];
        seq = unpacked[2];
        srcSystem = unpacked[3];
        srcComponent = unpacked[4];
        msgId = unpacked[5];
        header_len = 6;
        """);

    t.write(outf, """
}
    catch(e) {
        throw new Error('Unable to unpack MAVLink header: ' + e.message);
    }

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

    // is packet supposed to be signed?
    if ( incompat_flags & ${MAVHEAD}.MAVLINK_IFLAG_SIGNED ){
        signature_len = ${MAVHEAD}.MAVLINK_SIGNATURE_BLOCK_LEN;
    } else {
        signature_len = 0;
    }

    // header's declared len compared to packets actual len
    var actual_len = (msgbuf.length - (header_len + 2 + signature_len));
    var actual_len_nosign = (msgbuf.length - (header_len + 2 ));

    if ((mlen == actual_len) && (signature_len > 0)){
        var len_if_signed = mlen+signature_len;
        //console.log("Packet appears signed && labeled as signed, OK. msgId=" + msgId);

    } else  if ((mlen == actual_len_nosign) && (signature_len > 0)){

        var len_if_signed = mlen+signature_len;
        throw new Error("Packet appears unsigned when labeled as signed. Got actual_len "+actual_len_nosign+" expected " + len_if_signed + ", msgId=" + msgId);     

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

    }

    if (!(msgId in ${MAVHEAD}.map)) {
        throw new Error("Unknown MAVLink message ID (" + msgId + ")");
    }

    // here's the common chunks of packet we want to work with below..
    var payloadBuf = msgbuf.slice(${MAVHEAD}.HEADER_LEN, -(signature_len+2)); // the remaining bit between the header and the crc
    var crcCheckBuf = msgbuf.slice(1, -(signature_len+2)); // the part uses to calculate the crc - ie between the magic and signature,

    // decode the payload
    // refs: (fmt, type, order_map, crc_extra) = ${MAVHEAD}.map[msgId]
    var decoder = ${MAVHEAD}.map[msgId];

    // decode the checksum
    var receivedChecksum = undefined;
    if ( signature_len == 0 ) { // unsigned
        try {
            var crcBuf1 = msgbuf.slice(-2);
            receivedChecksum = jspack.Unpack('<H', crcBuf1);
        } catch (e) {
            throw new Error("Unable to unpack MAVLink unsigned CRC: " + e.message);
        }
    } else { // signed
        try {
            var crcBuf2 = msgbuf.slice(-15,-13);
            receivedChecksum = jspack.Unpack('<H', crcBuf2);
        } catch (e) {
            throw new Error("Unable to unpack MAVLink signed CRC: " + e.message);
        }
    }
    receivedChecksum = receivedChecksum[0];

    // make our own chksum of the relevant part of the packet...
    var messageChecksum = ${MAVHEAD}.x25Crc(crcCheckBuf);
    var messageChecksum2 = ${MAVHEAD}.x25Crc([decoder.crc_extra], messageChecksum);

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

    // now check the signature...
    var sig_ok = false 
    if (signature_len == ${MAVHEAD}.MAVLINK_SIGNATURE_BLOCK_LEN){
        this.signing.sig_count += 1
    }

    // it's a Buffer, zero-length means unused
    if (this.signing.secret_key.length != 0 ){
        var accept_signature = false;
        if (signature_len == ${MAVHEAD}.MAVLINK_SIGNATURE_BLOCK_LEN){
            sig_ok = this.check_signature(msgbuf, srcSystem, srcComponent);
            accept_signature = sig_ok;
            if (sig_ok){
                this.signing.goodsig_count += 1;
            }else{
                this.signing.badsig_count += 1;
            }
            if ( (! accept_signature) && (this.signing.allow_unsigned_callback != undefined) ){
                accept_signature = this.signing.allow_unsigned_callback(this, msgId);
                if (accept_signature){
                    this.signing.unsigned_count += 1;
                }else{
                    this.signing.reject_count += 1;
                }
            }
        }else if (this.signing.allow_unsigned_callback != undefined){
            accept_signature = this.signing.allow_unsigned_callback(this, msgId);
            if (accept_signature){
                this.signing.unsigned_count += 1;
            }else{
                this.signing.reject_count += 1;
            }
        }
        if (!accept_signature) throw new Error('Invalid signature');
    }

    // now look at the specifics of the payload...
    var paylen = jspack.CalcLength(decoder.format);

    """, {'MAVPROCESSOR': get_mavprocessor(xml),
          'MAVHEAD': get_mavhead(xml)})

    # Mavlink2 only
    if (xml.protocol_marker == 253):
        t.write(outf, """
//put any truncated 0's back in (ie zero-pad )
    if (paylen > payloadBuf.length) {
        payloadBuf = this.concat_buffer(payloadBuf, new Uint8Array(paylen - payloadBuf.length).fill(0));
    }
""")

    t.write(outf, """
    // Decode the payload and reorder the fields to match the order map.
    try {
        var t = jspack.Unpack(decoder.format, payloadBuf); 
    }
    catch (e) {
        throw new Error('Unable to unpack MAVLink payload type='+decoder.type+' format='+decoder.format+' payloadLength='+ payloadBuf +': '+ 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
        t.forEach(function(e, i, l) {
            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 check 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
        t.forEach(function(e, i, l) {
            args[i] = tempArgs[decoder.order_map[i]]
        });
    }

    // construct the message object
    try {
        // args at this point might look like:  { '0': 6, '1': 8, '2': 0, '3': 0, '4': 3, '5': 3 }
        var m = new decoder.type();   // make a new 'empty' instance of the right class,
        m.set(args,false); // associate ordered-field-numbers to names, after construction not during.
    }
    catch (e) {
        throw new Error('Unable to instantiate MAVLink message of type '+decoder.type+' : ' + e.message);
    }

    m._signed = sig_ok;
    if (m._signed) { m._link_id = msgbuf[-13]; }
 
    m._msgbuf = msgbuf;
    m._payload = payloadBuf;
    m.crc = receivedChecksum;
    m._header = new ${MAVHEAD}.header(msgId, mlen, seq, srcSystem, srcComponent, incompat_flags, compat_flags);
    this.log(m);
    return m;
}

""", {'MAVHEAD': get_mavhead(xml), 'MAVPROCESSOR': get_mavprocessor(xml), 'PROTOCOL_MARKER' : xml.protocol_marker})

def generate_footer(outf, xml):
    t.write(outf, """

// Browser and Node.js compatible module exports
if (!isNode) {
    // For browsers, attach to window or use global namespace
    if (typeof window !== 'undefined') {
        window.${MAVHEAD} = ${MAVHEAD};
        window.${MAVPROCESSOR} = ${MAVPROCESSOR};
    }
    // Also support global assignment
    if (typeof global !== 'undefined') {
        global.${MAVHEAD} = ${MAVHEAD};
        global.${MAVPROCESSOR} = ${MAVPROCESSOR};
    }
} else {
    // For Node.js, use module.exports
    if (typeof module === "object" && module.exports) {
        module.exports = {${MAVHEAD}, ${MAVPROCESSOR}};
    }
}

""", {'MAVHEAD': get_mavhead(xml), 'MAVPROCESSOR': get_mavprocessor(xml)})


#--------------------------------------tests start--------

def isfloat(value):
  try:
    float(value)
    return True
  except ValueError:
    return False

def generate_tests_preamble(outf, msgs, args, xml):
    print("Generating preamble")
    t.write(outf, """
/*
TESTS for MAVLink protocol implementation for node.js (auto-generated by mavgen_javascript.py)

Generated from: ${FILELIST}

Note: this file has been auto-generated. DO NOT EDIT
*/
var Long = require('long');

var {${MAVHEAD}, ${MAVPROCESSOR}} = require('./mavlink.js');

// mock mav with sysid-42 and componentid=150
let mav = new ${MAVPROCESSOR}(null, 42, 150);

// this uses the above mock by default, but lets us override it before or during tests if desired
let set_mav = function (_mav) {
    // set global mav var from local
    mav = _mav;
};
exports.set_mav = set_mav;

let verbose = 0; // 0 means not verbose, 1 means a bit more, 2 means most verbose
let set_verbose = function (_v) {
    // set global mav var from local
    verbose = _v;
};
exports.set_verbose = set_verbose;

// relevant to how we pass-in the Long object/s to jspack, we'll assume the calling user is smart enough to know that.
var wrap_long = function (someLong) {
    return [someLong.getLowBitsUnsigned(), someLong.getHighBitsUnsigned()];
}


""", {'FILELIST' : ",".join(args),
      'PROTOCOL_MARKER' : xml.protocol_marker,
      'crc_extra' : xml.crc_extra,
      'WIRE_PROTOCOL_VERSION' : ("2.0" if xml.protocol_marker == 253 else "1.0"),
      'MAVHEAD': get_mavhead(xml),
      'MAVPROCESSOR': get_mavprocessor(xml),
      'HEADERLEN': ("10" if xml.protocol_marker == 253 else "6")}
)


def generate_tests_mavlink_class(outf, msgs, xml):
    print("Generating MAVLink class")

    # Write mapper to enable decoding based on the integer message type
    #t.write(outf, "\n\n${MAVHEAD}.map = {\n", {'MAVHEAD': get_mavhead(xml)});
    for m in msgs:

        outf.write("let test_%s = function () {\n"% (  m.name.lower()));

        #var bs = new mavlink20.messages.battery_status(
        outf.write("   if ( verbose == 2 ) console.log('test creating and packing:%s'); \n" % (  m.name.lower() ) )
        outf.write("   if ( verbose == 1) { process.stdout.write('test creating and packing:"+m.name.lower()+"          \\r'); }\n")
        outf.write("   var test_%s = new %s.messages.%s(); \n" % (  m.name.lower(),get_mavhead(xml), m.name.lower()))
 
        idx = 0; # test data is in same order as ordered_fieldnames
        for f in m.ordered_fieldnames:
            tdata = m.test_data[idx] # test data
            #tdatatype = m.test_data_types[idx] # type of test data 
            fieldtype = m.ordered_fieldtypes[idx] # type of base field
            # wrap things non-number-like as strings, isnumeric() can't handle negatives, but conveniently none of the test suite uses negatives

            #print('testdata:'+tdata);
            #print('tdatatype:'+tdatatype);
            #print('fieldtype:'+fieldtype);
            #if tdatatype != fieldtype:
            #    sys.exit()
            
            _isnum = str(tdata).isdigit()
            _isarray = (tdata[0] == '[')
            _isfloat = isfloat(tdata)

            # javascript inconveniently considers bits >=128 in quite a lot of data types to be unicode, not binary, so we have to understand
            #  these and create them via Buffers and 'binary' or other whacky-doodle-ness here to be sure we get all the tests to pass.

            # array of chars
            if _isarray and (fieldtype == 'char'):
                tdata = 'new Buffer.from('+m.test_data[idx]+').toString("binary")'; # binary encoding here is important for bits >= 128
            # array of uint8_t ( like char )
            elif _isarray and  ( (fieldtype == 'uint8_t') or (fieldtype == 'int8_t')  ):
                tdata = 'new Buffer.from('+m.test_data[idx]+').toString("binary")';  # binary encoding here is important for bits >= 128
            # float/uint16_t/int16_t/int8_t/double array is apparently simple enough without Buffer wrapper
            elif _isarray and ( (fieldtype == 'float') or (fieldtype == 'uint16_t') or (fieldtype == 'int16_t') or (fieldtype == 'double') or ( fieldtype == 'int32_t' ) or ( fieldtype == 'uint32_t' ) ):
                tdata = m.test_data[idx];
            # array of other things
            elif _isarray:
                tdata = 'new Buffer.from('+m.test_data[idx]+') // generic buffer error?';  

            # https://github.com/birchroad/node-jspack/pull/4/commits/9828de064af42ab370009d3eeec7fc11be36b918
            elif fieldtype == 'uint64_t': # unsigned
                tdata =   'wrap_long(Long.fromString("'+m.test_data[idx]+'", true))'; # create unsigned Long from string, then rearrange Long object into 2x32bit unsigned ready for jspack
            elif fieldtype == 'int64_t': # signed
                tdata =   'wrap_long(Long.fromString("'+m.test_data[idx]+'", false))'; # same as above, but signed Long
            # special signed handling, to properly
            elif fieldtype == 'int8_t': # signed fields, we sometimes push raw value/s that exceed the min/max range of signed instead of using the correct sign
                tdata =  '(new Int8Array(['+m.test_data[idx]+']))[0]';  # basically a cast from unsigned int to signed int without sign bit loss
            # special signed handling, to properly
            elif fieldtype == 'int16_t': # signed fields, we sometimes push raw value/s that exceed the min/max range of signed instead of using the correct sign
                tdata =  '(new Int16Array(['+m.test_data[idx]+']))[0]';  # basically a cast from unsigned int to signed int without sign bit loss 
            # special signed handling, to properly 
            elif fieldtype == 'int32_t': # signed fields, we sometimes push raw value/s that exceed the min/max range of signed instead of using the correct sign
                tdata =  '(new Int32Array(['+m.test_data[idx]+']))[0]';  # basically a cast from unsigned int to signed int without sign bit loss

            elif _isfloat:
                    tdata = m.test_data[idx];
            elif _isnum:
                     tdata = m.test_data[idx];   
            else:      
                 #string
                 tdata = '"'+m.test_data[idx]+'"';

            outf.write( "      test_%s.%s = %s;" % ( m.name.lower(),f, tdata) );
            outf.write(" // fieldtype: %s "%(fieldtype));
            outf.write(" isarray: %s \n"%(_isarray));
            idx=idx+1;


        outf.write(" //var t = new Buffer.from([])\n; //%s\n"% (  m.name.lower()));
        outf.write(" var t = new Buffer.from(test_%s.pack(mav));\n"% (  m.name.lower()));

        outf.write("   return [test_%s,t]; // return an array of unpacked and packed options\n"% (  m.name.lower()));
        outf.write("};\n");
        outf.write("exports.test_%s = test_%s; // expose in module\n"% (  m.name.lower() ,m.name.lower() ) );
        outf.write("\n");
    

    outf.write(get_mavhead(xml)+"""Tests = function(){ \n""")

    for m in msgs:
        outf.write("test_%s();\n"% (  m.name.lower()));

    outf.write("};\n");
   
def generate_tests_footer(outf, xml):
    t.write(outf, """

// if run as an app, run the tests immediately, but if run as a module don't, require user to call
if (require.main === module) {
   verbose=2;  // 0 is not verbose, 1 is a bit, 2 is more.
   ${MAVHEAD}Tests();
} 


/* TESTs for MAVLink protocol handling class */
${MAVPROCESSOR}Tests = function() { ${MAVHEAD}Tests(); }
exports.${MAVPROCESSOR}Tests = ${MAVPROCESSOR}Tests; // expose in module

""", {'MAVHEAD': get_mavhead(xml), 'MAVPROCESSOR': get_mavprocessor(xml)})



def generate(basename, xml):
    '''generate complete javascript implementation'''

    if basename.endswith('.js'):
        filename = basename
    else:
        filename = basename + '.js'

    msgs = []
    enums = []
    filelist = []
    for x in xml:
        msgs.extend(x.message)
        enums.extend(x.enum)
        filelist.append(os.path.basename(x.filename))


    for m in msgs:
        m.fielddefaults = []
        if xml[0].little_endian:
            m.fmtstr = '<'
        else:
            m.fmtstr = '>'
        m.native_fmtstr = m.fmtstr

        # we've got instance support in generator, but not in the resultant code, yet.
        m.instance_field = None
        for f in m.ordered_fields:
            m.fmtstr += mavfmt(f)
            if f.instance:
                m.instance_field = f.name
                
        m.order_map = [0] * len(m.fieldnames)
        m.len_map = [0] * len(m.fieldnames)
        m.array_len_map = [0] * len(m.fieldnames)
        m.test_data = [0] * len(m.fieldnames)
        m.test_data_types = [0] * len(m.fieldnames)

        for i in range(0, len(m.fieldnames)):
            m.order_map[i] = m.ordered_fieldnames.index(m.fieldnames[i])
            m.ordered_fieldtypes[i] = m.ordered_fieldtypes[i]
            m.test_data[i] = str(m.ordered_fields[i].test_value)
            m.test_data_types[i] = str(m.ordered_fields[i].type)
            m.array_len_map[i] = m.ordered_fields[i].array_length
            
        for i in range(0, len(m.fieldnames)):
            n = m.order_map[i]
            m.len_map[n] = m.fieldlengths[i]





    print("Generating %s" % filename)
    outf = open(filename, "w")
    generate_preamble(outf, msgs, filelist, xml[0])
    generate_enums(outf, enums, xml[0])
    generate_message_ids(outf, msgs, xml[0])
    generate_classes(outf, msgs, xml[0])
    generate_mavlink_class(outf, msgs, xml[0])
    generate_footer(outf, xml[0])
    outf.close()
    print("Generated %s OK" % filename)

    testfilename = filename.replace('.js','.tests.js')
    print("Generating TESTS %s" % testfilename)
    outf = open(testfilename, "w")
    generate_tests_preamble(outf, msgs, filelist, xml[0])
    generate_tests_mavlink_class(outf, msgs, xml[0])
    generate_tests_footer(outf, xml[0])
    outf.close()
    print("Generating TESTS %s" % testfilename)

