#! /usr/bin/env python3
# Read from stdin, spit out C header or body.

import argparse
import copy
import fileinput
import re

from collections import namedtuple

Enumtype = namedtuple('Enumtype', ['name', 'value'])

type2size = {
    'pad': 1,
    'struct channel_id': 32,
    'struct short_channel_id': 8,
    'struct ipv6': 16,
    'secp256k1_ecdsa_signature': 64,
    'struct preimage': 32,
    'struct pubkey': 33,
    'struct sha256': 32,
    'struct bitcoin_blkid': 32,
    'struct bitcoin_txid': 32,
    'u64': 8,
    'u32': 4,
    'u16': 2,
    'u8': 1,
    'bool': 1
}

# These struct array helpers require a context to allocate from.
varlen_structs = [
    'gossip_getnodes_entry',
    'failed_htlc',
    'utxo',
    'bitcoin_tx',
    'wirestring',
]


class FieldType(object):
    def __init__(self, name):
        self.name = name

    def is_assignable(self):
        return self.name in ['u8', 'u16', 'u32', 'u64', 'bool'] or self.name.startswith('enum ')

    # We only accelerate the u8 case: it's common and trivial.
    def has_array_helper(self):
        return self.name in ['u8']

    # Returns base size
    @staticmethod
    def _typesize(typename):
        if typename in type2size:
            return type2size[typename]
        elif typename.startswith('struct ') or typename.startswith('enum '):
            # We allow unknown structures/enums, for extensibility (can only happen
            # if explicitly specified in csv)
            return 0
        else:
            raise ValueError('Unknown typename {}'.format(typename))


# Full (message, fieldname)-mappings
typemap = {
    ('update_fail_htlc', 'reason'): FieldType('u8'),
    ('node_announcement', 'alias'): FieldType('u8'),
    ('update_add_htlc', 'onion_routing_packet'): FieldType('u8'),
    ('update_fulfill_htlc', 'payment_preimage'): FieldType('struct preimage'),
    ('error', 'data'): FieldType('u8'),
    ('shutdown', 'scriptpubkey'): FieldType('u8'),
    ('node_announcement', 'rgb_color'): FieldType('u8'),
    ('node_announcement', 'addresses'): FieldType('u8'),
    ('node_announcement', 'ipv6'): FieldType('struct ipv6'),
    ('announcement_signatures', 'short_channel_id'): FieldType('struct short_channel_id'),
    ('channel_announcement', 'short_channel_id'): FieldType('struct short_channel_id'),
    ('channel_update', 'short_channel_id'): FieldType('struct short_channel_id')
}

# Partial names that map to a datatype
partialtypemap = {
    'signature': FieldType('secp256k1_ecdsa_signature'),
    'features': FieldType('u8'),
    'channel_id': FieldType('struct channel_id'),
    'chain_hash': FieldType('struct bitcoin_blkid'),
    'funding_txid': FieldType('struct bitcoin_txid'),
    'pad': FieldType('pad'),
}

# Size to typename match
sizetypemap = {
    33: FieldType('struct pubkey'),
    32: FieldType('struct sha256'),
    8: FieldType('u64'),
    4: FieldType('u32'),
    2: FieldType('u16'),
    1: FieldType('u8')
}


# It would be nicer if we had put '*u8' in spec and disallowed bare lenvar.
# In practice we only recognize lenvar when it's the previous field.

# size := baresize | arraysize
# baresize := simplesize | lenvar
# simplesize := number | type
# arraysize := length '*' type
# length := lenvar | number
class Field(object):
    def __init__(self, message, name, size, comments, prevname):
        self.message = message
        self.comments = comments
        self.name = name
        self.is_len_var = False
        self.lenvar = None
        self.num_elems = 1

        # If it's an arraysize, swallow prefix.
        if '*' in size:
            number = size.split('*')[0]
            if number == prevname:
                self.lenvar = number
            else:
                self.num_elems = int(number)
            size = size.split('*')[1]
        elif options.bolt and size == prevname:
            # Raw length field, implies u8.
            self.lenvar = size
            size = '1'

        # Bolts use just a number: Guess type based on size.
        if options.bolt:
            base_size = int(size)
            self.fieldtype = Field._guess_type(message, self.name, base_size)
            # There are some arrays which we have to guess, based on sizes.
            tsize = FieldType._typesize(self.fieldtype.name)
            if base_size % tsize != 0:
                raise ValueError('Invalid size {} for {}.{} not a multiple of {}'
                                 .format(base_size,
                                         self.message,
                                         self.name,
                                         tsize))
            self.num_elems = int(base_size / tsize)
        else:
            # Real typename.
            self.fieldtype = FieldType(size)

    def basetype(self):
        base = self.fieldtype.name
        if base.startswith('struct '):
            base = base[7:]
        elif base.startswith('enum '):
            base = base[5:]
        return base

    def is_padding(self):
        return self.name.startswith('pad')

    # Padding is always treated as an array.
    def is_array(self):
        return self.num_elems > 1 or self.is_padding()

    def is_variable_size(self):
        return self.lenvar is not None

    def is_assignable(self):
        if self.is_array() or self.is_variable_size():
            return False
        return self.fieldtype.is_assignable()

    def has_array_helper(self):
        return self.fieldtype.has_array_helper()

    # Returns FieldType
    @staticmethod
    def _guess_type(message, fieldname, base_size):
        # Check for full (message, fieldname)-matches
        if (message, fieldname) in typemap:
            return typemap[(message, fieldname)]

        # Check for partial field names
        for k, v in partialtypemap.items():
            if k in fieldname:
                return v

        # Check for size matches
        if base_size in sizetypemap:
            return sizetypemap[base_size]

        raise ValueError('Unknown size {} for {}'.format(base_size, fieldname))


fromwire_impl_templ = """bool fromwire_{name}({ctx}const void *p{args})
{{
{fields}
        const u8 *cursor = p;
        size_t plen = tal_len(p);

        if (fromwire_u16(&cursor, &plen) != {enum.name})
                return false;
{subcalls}
        return cursor != NULL;
}}
"""

fromwire_header_templ = """bool fromwire_{name}({ctx}const void *p{args});
"""

towire_header_templ = """u8 *towire_{name}(const tal_t *ctx{args});
"""
towire_impl_templ = """u8 *towire_{name}(const tal_t *ctx{args})
{{
{field_decls}
        u8 *p = tal_arr(ctx, u8, 0);
        towire_u16(&p, {enumname});
{subcalls}

        return memcheck(p, tal_count(p));
}}
"""

printwire_header_templ = """void printwire_{name}(const u8 *cursor);
"""
printwire_impl_templ = """void printwire_{name}(const u8 *cursor)
{{
        size_t plen = tal_len(cursor);

        if (fromwire_u16(&cursor, &plen) != {enum.name}) {{
                printf("WRONG TYPE?!\\n");
                return;
        }}

{subcalls}

        if (plen != 0)
                printf("EXTRA: %s\\n", tal_hexstr(NULL, cursor, plen));
}}
"""


class Message(object):
    def __init__(self, name, enum, comments):
        self.name = name
        self.enum = enum
        self.comments = comments
        self.fields = []
        self.has_variable_fields = False

    def checkLenField(self, field):
        for f in self.fields:
            if f.name == field.lenvar:
                if f.fieldtype.name != 'u16':
                    raise ValueError('Field {} has non-u16 length variable {} (type {})'
                                     .format(field.name, field.lenvar, f.fieldtype.name))

                if f.is_array() or f.is_variable_size():
                    raise ValueError('Field {} has non-simple length variable {}'
                                     .format(field.name, field.lenvar))
                f.is_len_var = True
                f.lenvar_for = field
                return
        raise ValueError('Field {} unknown length variable {}'
                         .format(field.name, field.lenvar))

    def addField(self, field):
        # We assume field lengths are 16 bit, to avoid overflow issues and
        # massive allocations.
        if field.is_variable_size():
            self.checkLenField(field)
            self.has_variable_fields = True
        elif field.basetype() in varlen_structs:
            self.has_variable_fields = True
        self.fields.append(field)

    def print_fromwire_array(self, subcalls, basetype, f, name, num_elems):
        if f.has_array_helper():
            subcalls.append('\tfromwire_{}_array(&cursor, &plen, {}, {});'
                            .format(basetype, name, num_elems))
        else:
            subcalls.append('\tfor (size_t i = 0; i < {}; i++)'
                            .format(num_elems))
            if f.fieldtype.is_assignable():
                subcalls.append('\t\t({})[i] = fromwire_{}(&cursor, &plen);'
                                .format(name, basetype))
            elif basetype in varlen_structs:
                subcalls.append('\t\t({})[i] = fromwire_{}(ctx, &cursor, &plen);'
                                .format(name, basetype))
            else:
                subcalls.append('\t\tfromwire_{}(&cursor, &plen, {} + i);'
                                .format(basetype, name))

    def print_fromwire(self, is_header):
        ctx_arg = 'const tal_t *ctx, ' if self.has_variable_fields else ''

        args = []

        for f in self.fields:
            if f.is_len_var or f.is_padding():
                continue
            elif f.is_array():
                args.append(', {} {}[{}]'.format(f.fieldtype.name, f.name, f.num_elems))
            else:
                ptrs = '*'
                # If we're handing a variable array, we need a ptr-to-ptr.
                if f.is_variable_size():
                    ptrs += '*'
                # If each type is a variable length, we need a ptr to that.
                if f.basetype() in varlen_structs:
                    ptrs += '*'

                args.append(', {} {}{}'.format(f.fieldtype.name, ptrs, f.name))

        template = fromwire_header_templ if is_header else fromwire_impl_templ
        fields = ['\t{} {};\n'.format(f.fieldtype.name, f.name) for f in self.fields if f.is_len_var]

        subcalls = []
        for f in self.fields:
            basetype = f.basetype()

            for c in f.comments:
                subcalls.append('\t/*{} */'.format(c))

            if f.is_padding():
                subcalls.append('\tfromwire_pad(&cursor, &plen, {});'
                                .format(f.num_elems))
            elif f.is_array():
                self.print_fromwire_array(subcalls, basetype, f, f.name,
                                          f.num_elems)
            elif f.is_variable_size():
                subcalls.append("\t//2nd case {name}".format(name=f.name))
                typename = f.fieldtype.name
                # If structs are varlen, need array of ptrs to them.
                if basetype in varlen_structs:
                    typename += ' *'
                subcalls.append('\t*{} = {} ? tal_arr(ctx, {}, {}) : NULL;'
                                .format(f.name, f.lenvar, typename, f.lenvar))

                self.print_fromwire_array(subcalls, basetype, f, '*' + f.name,
                                          f.lenvar)
            elif f.is_assignable():
                subcalls.append("\t//3th case {name}".format(name=f.name))
                if f.is_len_var:
                    subcalls.append('\t{} = fromwire_{}(&cursor, &plen);'
                                    .format(f.name, basetype))
                else:
                    subcalls.append('\t*{} = fromwire_{}(&cursor, &plen);'
                                    .format(f.name, basetype))
            elif basetype in varlen_structs:
                subcalls.append('\t*{} = fromwire_{}(ctx, &cursor, &plen);'
                                .format(f.name, basetype))
            else:
                subcalls.append('\tfromwire_{}(&cursor, &plen, {});'
                                .format(basetype, f.name))

        return template.format(
            name=self.name,
            ctx=ctx_arg,
            args=''.join(args),
            fields=''.join(fields),
            enum=self.enum,
            subcalls='\n'.join(subcalls)
        )

    def print_towire_array(self, subcalls, basetype, f, num_elems):
        if f.has_array_helper():
            subcalls.append('\ttowire_{}_array(&p, {}, {});'
                            .format(basetype, f.name, num_elems))
        else:
            subcalls.append('\tfor (size_t i = 0; i < {}; i++)\n'
                            .format(num_elems))
            if f.fieldtype.is_assignable() or basetype in varlen_structs:
                subcalls.append('\t\ttowire_{}(&p, {}[i]);'
                                .format(basetype, f.name))
            else:
                subcalls.append('\t\ttowire_{}(&p, {} + i);'
                                .format(basetype, f.name))

    def print_towire(self, is_header):
        template = towire_header_templ if is_header else towire_impl_templ
        args = []
        for f in self.fields:
            if f.is_padding() or f.is_len_var:
                continue
            if f.is_array():
                args.append(', const {} {}[{}]'.format(f.fieldtype.name, f.name, f.num_elems))
            elif f.is_assignable():
                args.append(', {} {}'.format(f.fieldtype.name, f.name))
            elif f.is_variable_size() and f.basetype() in varlen_structs:
                args.append(', const {} **{}'.format(f.fieldtype.name, f.name))
            else:
                args.append(', const {} *{}'.format(f.fieldtype.name, f.name))

        field_decls = []
        for f in self.fields:
            if f.is_len_var:
                field_decls.append('\t{0} {1} = tal_count({2});'.format(
                    f.fieldtype.name, f.name, f.lenvar_for.name
                ))

        subcalls = []
        for f in self.fields:
            basetype = f.fieldtype.name
            if basetype.startswith('struct '):
                basetype = basetype[7:]
            elif basetype.startswith('enum '):
                basetype = basetype[5:]

            for c in f.comments:
                subcalls.append('\t/*{} */'.format(c))

            if f.is_padding():
                subcalls.append('\ttowire_pad(&p, {});'
                                .format(f.num_elems))
            elif f.is_array():
                self.print_towire_array(subcalls, basetype, f, f.num_elems)
            elif f.is_variable_size():
                self.print_towire_array(subcalls, basetype, f, f.lenvar)
            else:
                subcalls.append('\ttowire_{}(&p, {});'
                                .format(basetype, f.name))

        return template.format(
            name=self.name,
            args=''.join(args),
            enumname=self.enum.name,
            field_decls='\n'.join(field_decls),
            subcalls='\n'.join(subcalls),
        )

    def add_truncate_check(self, subcalls, indent='\t'):
        # Report if truncated, otherwise print.
        subcalls.append(indent + 'if (!cursor) {')
        subcalls.append(indent + '\tprintf("**TRUNCATED**\\n");')
        subcalls.append(indent + '\treturn;')
        subcalls.append(indent + '}')

    def print_printwire_array(self, subcalls, basetype, f, num_elems):
        if f.has_array_helper():
            subcalls.append('\tprintwire_{}_array(&cursor, &plen, {});'
                            .format(basetype, num_elems))
        else:
            subcalls.append('\tprintf("[");')
            subcalls.append('\tfor (size_t i = 0; i < {}; i++) {{'
                            .format(num_elems))
            subcalls.append('\t\t{} v;'.format(f.fieldtype.name))
            if f.fieldtype.is_assignable():
                subcalls.append('\t\tv = fromwire_{}(&cursor, plen);'
                                .format(f.fieldtype.name, basetype))
            else:
                # We don't handle this yet!
                assert(basetype not in varlen_structs)

                subcalls.append('\t\tfromwire_{}(&cursor, &plen, &v);'
                                .format(basetype))

            self.add_truncate_check(subcalls, indent='\t\t')

            subcalls.append('\t\tprintwire_{}(&v);'.format(basetype))
            subcalls.append('\t}')
            subcalls.append('\tprintf("]");')

    def print_printwire(self, is_header):
        template = printwire_header_templ if is_header else printwire_impl_templ
        fields = ['\t{} {};\n'.format(f.fieldtype.name, f.name) for f in self.fields if f.is_len_var]

        subcalls = []
        for f in self.fields:
            basetype = f.basetype()

            for c in f.comments:
                subcalls.append('\t/*{} */'.format(c))

            if f.is_len_var:
                subcalls.append('\t{} {} = fromwire_{}(&cursor, &plen);'
                                .format(f.fieldtype.name, f.name, basetype))
                self.add_truncate_check(subcalls)
                continue

            subcalls.append('\tprintf("{}=");'.format(f.name))
            if f.is_padding():
                subcalls.append('\tprintwire_pad(&cursor, &plen, {});'
                                .format(f.num_elems))
                self.add_truncate_check(subcalls)
            elif f.is_array():
                self.print_printwire_array(subcalls, basetype, f, f.num_elems)
                self.add_truncate_check(subcalls)
            elif f.is_variable_size():
                self.print_printwire_array(subcalls, basetype, f, f.lenvar)
                self.add_truncate_check(subcalls)
            else:
                if f.is_assignable():
                    subcalls.append('\t{} {} = fromwire_{}(&cursor, &plen);'
                                    .format(f.fieldtype.name, f.name, basetype))
                else:
                    # Don't handle these yet.
                    assert(basetype not in varlen_structs)
                    subcalls.append('\t{} {};'.
                                    format(f.fieldtype.name, f.name))
                    subcalls.append('\tfromwire_{}(&cursor, &plen, &{});'
                                    .format(basetype, f.name))

                self.add_truncate_check(subcalls)
                subcalls.append('\tprintwire_{}(&{});'
                                .format(basetype, f.name))

        return template.format(
            name=self.name,
            fields=''.join(fields),
            enum=self.enum,
            subcalls='\n'.join(subcalls)
        )


def find_message(messages, name):
    for m in messages:
        if m.name == name:
            return m

    return None


def find_message_with_option(messages, optional_messages, name, option):
    fullname = name + "_" + option.replace('-', '_')

    base = find_message(messages, name)
    if not base:
        raise ValueError('Unknown message {}'.format(name))

    m = find_message(optional_messages, fullname)
    if not m:
        # Add a new option.
        m = copy.deepcopy(base)
        m.name = fullname
        optional_messages.append(m)
    return m


parser = argparse.ArgumentParser(description='Generate C from CSV')
parser.add_argument('--header', action='store_true', help="Create wire header")
parser.add_argument('--bolt', action='store_true', help="Generate wire-format for BOLT")
parser.add_argument('--printwire', action='store_true', help="Create print routines")
parser.add_argument('headerfilename', help='The filename of the header')
parser.add_argument('enumname', help='The name of the enum to produce')
parser.add_argument('files', nargs='*', help='Files to read in (or stdin)')
options = parser.parse_args()

# Maps message names to messages
messages = []
messages_with_option = []
comments = []
includes = []
prevfield = None

# Read csv lines.  Single comma is the message values, more is offset/len.
for line in fileinput.input(options.files):
    # #include gets inserted into header
    if line.startswith('#include '):
        includes.append(line)
        continue

    by_comments = line.rstrip().split('#')

    # Emit a comment if they included one
    if by_comments[1:]:
        comments.append(' '.join(by_comments[1:]))

    parts = by_comments[0].split(',')
    if parts == ['']:
        continue

    if len(parts) == 2:
        # eg commit_sig,132
        messages.append(Message(parts[0], Enumtype("WIRE_" + parts[0].upper(), parts[1]), comments))
        comments = []
        prevfield = None
    else:
        if len(parts) == 4:
            # eg commit_sig,0,channel-id,8 OR
            #    commit_sig,0,channel-id,u64
            m = find_message(messages, parts[0])
            if m is None:
                raise ValueError('Unknown message {}'.format(parts[0]))
        elif len(parts) == 5:
            # eg.
            # channel_reestablish,48,your_last_per_commitment_secret,32,option209
            m = find_message_with_option(messages, messages_with_option, parts[0], parts[4])
        else:
            raise ValueError('Line {} malformed'.format(line.rstrip()))

        f = Field(m.name, parts[2], parts[3], comments, prevfield)
        m.addField(f)
        # If it used prevfield as lenvar, keep that for next
        # time (multiple fields can use the same lenvar).
        if not f.lenvar:
            prevfield = parts[2]
        comments = []

header_template = """/* This file was generated by generate-wire.py */
/* Do not modify this file! Modify the _csv file it was generated from. */
#ifndef LIGHTNING_{idem}
#define LIGHTNING_{idem}
#include <ccan/tal/tal.h>
#include <wire/wire.h>
{includes}
enum {enumname} {{
{enums}}};
const char *{enumname}_name(int e);

{func_decls}
#endif /* LIGHTNING_{idem} */
"""

impl_template = """/* This file was generated by generate-wire.py */
/* Do not modify this file! Modify the _csv file it was generated from. */
#include <{headerfilename}>
#include <ccan/mem/mem.h>
#include <ccan/tal/str/str.h>
#include <stdio.h>

const char *{enumname}_name(int e)
{{
        static char invalidbuf[sizeof("INVALID ") + STR_MAX_CHARS(e)];

        switch ((enum {enumname})e) {{
        {cases}
        }}

        sprintf(invalidbuf, "INVALID %i", e);
        return invalidbuf;
}}

{func_decls}
"""

print_header_template = """/* This file was generated by generate-wire.py */
/* Do not modify this file! Modify the _csv file it was generated from. */
#ifndef LIGHTNING_{idem}
#define LIGHTNING_{idem}
#include <ccan/tal/tal.h>
#include <devtools/print_wire.h>
{includes}

void print_message(const u8 *msg);

{func_decls}
#endif /* LIGHTNING_{idem} */
"""

print_template = """/* This file was generated by generate-wire.py */
/* Do not modify this file! Modify the _csv file it was generated from. */
#include "{headerfilename}"
#include <ccan/mem/mem.h>
#include <ccan/tal/str/str.h>
#include <common/utils.h>
#include <stdio.h>

void print_message(const u8 *msg)
{{
        switch ((enum {enumname})fromwire_peektype(msg)) {{
        {printcases}
        }}

        printf("UNKNOWN: %s\\n", tal_hex(msg, msg));
}}

{func_decls}
"""

idem = re.sub(r'[^A-Z]+', '_', options.headerfilename.upper())
if options.printwire:
    if options.header:
        template = print_header_template
    else:
        template = print_template
elif options.header:
    template = header_template
else:
    template = impl_template

# Dump out enum, sorted by value order.
enums = ""
for m in messages:
    for c in m.comments:
        enums += '\t/*{} */\n'.format(c)
    enums += '\t{} = {},\n'.format(m.enum.name, m.enum.value)
includes = '\n'.join(includes)
cases = ['case {enum.name}: return "{enum.name}";'.format(enum=m.enum) for m in messages]
printcases = ['case {enum.name}: printf("{enum.name}:\\n"); printwire_{name}(msg); return;'.format(enum=m.enum, name=m.name) for m in messages]

if options.printwire:
    decls = [m.print_printwire(options.header) for m in messages + messages_with_option]
else:
    fromwire_decls = [m.print_fromwire(options.header) for m in messages + messages_with_option]
    towire_decls = towire_decls = [m.print_towire(options.header) for m in messages + messages_with_option]
    decls = fromwire_decls + towire_decls

print(template.format(
    headerfilename=options.headerfilename,
    cases='\n\t'.join(cases),
    printcases='\n\t'.join(printcases),
    idem=idem,
    includes=includes,
    enumname=options.enumname,
    enums=enums,
    func_decls='\n'.join(decls)))
