#!/usr/bin/env python
# BlueKtichen GmbH (c) 2014

import glob
import re
import sys
import os

print '''
Java binding generator for BTstack
Copyright 2014, BlueKitchen GmbH
'''

# com.bluekitchen.btstack.BTstack.java templates
java_btstack_header = \
'''/** 
 * BTstack Client Library
 */

package %s;

public class BTstack extends BTstackClient {
    
'''
java_btstack_command = '''
    public boolean %s(%s){
        // %s
        int command_len = %s;
        byte[] command = new byte[command_len];
        Util.storeBt16(command, 0, Util.opcode(%s, %s));
        int offset = 2;
        Util.storeByte(command, offset, command_len - 3);
        offset++;
%s
        Packet packet = new Packet(Packet.HCI_COMMAND_PACKET, 0, command, command.length);
        return sendPacket(packet);
    }
'''
java_btstack_footer = '''
}
'''

# com.bluekitchen.btstack.EventFactory template
java_event_factory_template = \
'''package {0};

import {0}.event.*;

public class EventFactory {{

    /** @brief event codes */

{1}
    public static Event eventForPacket(Packet packet){{
        int eventType = Util.readByte(packet.getBuffer(), 0);
        switch (eventType){{
{2}        
        case 0x3e:  // LE_META_EVENT
            int subEventType = Util.readByte(packet.getBuffer(), 2);
            switch (subEventType){{
{3}            
            default:
                return new Event(packet);
            }}

        default:
            return new Event(packet);
        }}
    }}
}}
'''
java_event_factory_event = '''
        case {0}:
            return new {1}(packet);
'''
java_event_factory_subevent = '''
            case {0}:
                return new {1}(packet);
'''

# com.bluekitchen.btstack.events.* template
java_event_template = \
'''package {0}.event;

import {0}.*;

public class {1} extends Event {{

    public {1}(Packet packet) {{
        super(packet);
    }}
    {2}
    {3}
}}
'''

java_event_getter = \
'''
    /**
     * @return {1} as {0}
     */
    public {0} get{1}(){{
        {2}
    }}
'''

java_event_getter_data = \
'''int len = get{0}();
        byte[] result = new byte[len];
        System.arraycopy(data, {1}, result, 0, len);
        return result;'''

java_event_getter_remaining_data = \
'''int len = getPayloadLen() - {0};
        byte[] result = new byte[len];
        System.arraycopy(data, {0}, result, 0, len);
        return result;'''

java_event_to_string = \
'''
    public String toString(){{
        StringBuffer t = new StringBuffer();
        t.append("{0} < type = ");
        t.append(String.format("0x%02x, ", getEventType()));
        t.append(getEventType());
{1}        t.append(" >");
        return t.toString();
    }}
'''


# global variables/defines
package='com.bluekitchen.btstack'
gen_path = 'gen/' + package.replace('.', '/')
hci_cmds_h_path = '../include/btstack/hci_cmds.h'
hci_cmds_c_path = '../src/hci_cmds.c'
hci_h_path = '../src/hci.h'

defines = dict()
defines_used = set()

def assert_dir(path):
    if not os.access(path, os.R_OK):
        os.makedirs(path)

def cap(x):
    if x.lower() == 'btstack':
        return 'BTstack'
    acronyms = ['GAP', 'GATT', 'HCI', 'L2CAP', 'LE', 'RFCOMM', 'SM', 'SDP', 'UUID16', 'UUID128']
    if x.upper() in acronyms:
        return x.upper()
    return x.capitalize()

def camel_case(name):
    return ''.join(map(cap, name.split('_')))

def camel_case_var(name):
    if name in ['uuid128', 'uuid16']:
        return name
    camel = camel_case(name)
    return camel[0].lower() + camel[1:]

def read_defines(infile):
    global defines
    with open (infile, 'rb') as fin:

        for line in fin:
            parts = re.match('#define\s+(\w+)\s+(\w*)',line)
            if parts and len(parts.groups()) == 2:
                (key, value) = parts.groups()
                defines[key] = value

def java_type_for_btstack_type(type):
    param_types = { '1' : 'int', '2' : 'int', '3' : 'int', '4' : 'long', 'H' : 'int', 'B' : 'BD_ADDR',
                    'D' : 'byte []', 'E' : 'byte [] ', 'N' : 'String' , 'P' : 'byte []', 'A' : 'byte []',
                    'R' : 'byte []', 'S' : 'byte []',
                    'J' : 'int', 'L' : 'int', 'V' : 'byte []', 'U' : 'BT_UUID',
                    'X' : 'GATTService', 'Y' : 'GATTCharacteristic', 'Z' : 'GATTCharacteristicDescriptor',
                    'T' : 'String'}
    return param_types[type]

def size_for_type(type):
    param_sizes = { '1' : 1, '2' : 2, '3' : 3, '4' : 4, 'H' : 2, 'B' : 6, 'D' : 8, 'E' : 240, 'N' : 248, 'P' : 16,
                    'A' : 31, 'S' : -1, 'V': -1, 'J' : 1, 'L' : 2, 'U' : 16, 'X' : 20, 'Y' : 24, 'Z' : 18, 'T':-1}
    return param_sizes[type]

def create_command_java(fout, name, ogf, ocf, format, params):
    global java_btstack_command

    ind = '        '
    param_store = {
     '1' : 'Util.storeByte(command, offset, %s);',
     'J' : 'Util.storeByte(command, offset, %s);',
     '2' : 'Util.storeBt16(command, offset, %s);',
     'H' : 'Util.storeBt16(command, offset, %s);',
     'L' : 'Util.storeBt16(command, offset, %s);',
     '3' : 'Util.storeBt24(command, offset, %s);',
     '4' : 'Util.storeBt32(command, offset, %s);',
     'D' : 'Util.storeBytes(command, offset, %s, 8);',
     'E' : 'Util.storeBytes(command, offset, %s, 240);',
     'P' : 'Util.storeBytes(command, offset, %s, 16);',
     'A' : 'Util.storeBytes(command, offset, %s, 31);',
     'S' : 'Util.storeBytes(command, offset, %s);', 
     'B' : 'Util.storeBytes(command, offset, %s.getBytes());',
     'U' : 'Util.storeBytes(command, offset, %s.getBytes());', 
     'X' : 'Util.storeBytes(command, offset, %s.getBytes());', 
     'Y' : 'Util.storeBytes(command, offset, %s.getBytes());', 
     'Z' : 'Util.storeBytes(command, offset, %s.getBytes());', 
     'N' : 'Util.storeString(command, offset, %s, 248);',
     }
    # method arguments 
    arg_counter = 1
    args = []
    for param_type, arg_name in zip(format, params):
        arg_type = java_type_for_btstack_type(param_type)
        arg_size = size_for_type(param_type)
        arg = (param_type, arg_type, arg_size, arg_name)
        args.append(arg)
        arg_counter += 1

    # method argument declaration 
    args2 = []
    for arg in args:
        args2.append('%s %s' % (arg[1], arg[3]))
    args_string = ', '.join(args2)

    # command size (opcode, len)
    size_fixed = 3
    size_var = ''
    for arg in args:
        size = arg[2]
        if size > 0:
            size_fixed += size
        else:
            size_var += ' + %s.length' % arg[3]
    size_string = '%u%s' % (size_fixed, size_var)

    store_params = ''

    length_name = ''
    for (param_type, arg_type, arg_size, arg_name) in args:
        if param_type in ['L', 'J']:
            length_name = arg_name
        if param_type == 'V':
            store_params += ind + 'Util.storeBytes(command, offset, %s, %s);' % (arg_name, length_name) + '\n';
            store_params += ind + 'offset += %s;\n' % length_name;
            length_name = ''
        else:
            store_params += ind + (param_store[param_type] % arg_name) + '\n';
            size = arg_size
            if size > 0:
                store_params += ind + 'offset += %u;\n' % arg_size;
            else:
                store_params += ind + 'offset += %s.length;\n' % arg_name

    fout.write( java_btstack_command % (name, args_string, format, size_string, ogf, ocf, store_params))

def mark_define_as_used(term):
    if term.startswith('0'):
        return
    defines_used.add(term)

def java_define_string(key):
    global defines
    return '    public static final int %s = %s;\n' % (key, defines[key])

def java_defines_string(keys):
    return '\n'.join( map(java_define_string, sorted(keys)))

def parse_commands(infile):

    global gen_path
    assert_dir(gen_path)
    
    outfile = '%s/BTstack.java' % gen_path

    with open(outfile, 'w') as fout:
    
        fout.write(java_btstack_header % package)

        with open (infile, 'rb') as fin:

            params = []
            for line in fin:

                parts = re.match('.*@param\s*(\w*)\s*', line)
                if parts and len(parts.groups()) == 1:
                    param = parts.groups()[0]
                    params.append(camel_case_var(param))
                    continue

                declaration = re.match('const\s+hci_cmd_t\s+(\w+)[\s=]+', line)
                if declaration:
                    command_name = camel_case(declaration.groups()[0])
                    if command_name.endswith('Cmd'):
                        command_name = command_name[:-len('Cmd')]
                    continue

                definition = re.match('\s*OPCODE\\(\s*(\w+)\s*,\s+(\w+)\s*\\)\s*,\s\\"(\w*)\\".*', line)
                if definition:
                    (ogf, ocf, format) = definition.groups()
                    if len(params) != len(format):
                        params = []
                        arg_counter = 1
                        for f in format:
                            arg_name = 'arg%u' % arg_counter
                            params.append(arg_name)
                            arg_counter += 1
                    create_command_java(fout, command_name, ogf, ocf, format, params);
                    mark_define_as_used(ogf)
                    mark_define_as_used(ocf)
                    params = []
                    continue

        fout.write('\n    /** defines used */\n\n')
        for key in sorted(defines_used):
            fout.write(java_define_string(key))

        fout.write(java_btstack_footer)

def create_event(event_name, format, args):
    global gen_path
    global package
    global java_event_template

    param_read = {
     '1' : 'return Util.readByte(data, %u);',
     'J' : 'return Util.readByte(data, %u);',
     '2' : 'return Util.readBt16(data, %u);',
     '3' : 'return Util.readBt24(data, %u);',
     '4' : 'return Util.readBt32(data, %u);',
     'H' : 'return Util.readBt16(data, %u);',
     'L' : 'return Util.readByte(data, %u);',
     'B' : 'return Util.readBdAddr(data, %u);',
     'X' : 'return Util.readGattService(data, %u);',
     'Y' : 'return Util.readGattCharacteristic(data, %u);',
     'Z' : 'return Util.readGattCharacteristicDescriptor(data, %u);',
     'T' : 'int offset = %u; \n        return Util.getText(data, offset, getPayloadLen()-offset);',
     'N' : 'return Util.getText(data, %u, 248);',
     # 'D' : 'Util.storeBytes(data, %u, 8);',
     # 'E' : 'Util.storeBytes(data, %u, 240);',
     # 'P' : 'Util.storeBytes(data, %u, 16);',
     # 'A' : 'Util.storeBytes(data, %u, 31);',
     # 'S' : 'Util.storeBytes(data, %u);'
     }

    gen_event_path = '%s/event' % gen_path
    outfile = '%s/%s.java' % (gen_event_path, event_name)
    with open(outfile, 'w') as fout:
        offset = 2
        getters = ''
        length_name = ''
        for f, arg in zip(format, args):
            # just remember name
            if f in ['L','J']:
                length_name = camel_case(arg)
            if f == 'R':    
                # remaining data
                access = java_event_getter_remaining_data.format(offset)
                size = 0
            elif f == 'V':
                access = java_event_getter_data.format(length_name, offset)
                size = 0
            else: 
                access = param_read[f] % offset
                size = size_for_type(f)
            getters += java_event_getter.format(java_type_for_btstack_type(f), camel_case(arg), access)
            offset += size
        to_string_args = ''
        for arg in args:
            to_string_args += '        t.append(", %s = ");\n' % arg
            to_string_args += '        t.append(get%s());\n' % camel_case(arg)
        to_string_method = java_event_to_string.format(event_name, to_string_args)
        fout.write(java_event_template.format(package, event_name, getters, to_string_method))

def create_events(events):
    global gen_path
    gen_path_events = gen_path + '/event'
    assert_dir(gen_path_events)

    for event_type, event_name, format, args in events:
        event_name = camel_case(event_name)
        create_event(event_name, format, args)

def create_event_factory(events, le_events, defines):
    global gen_path
    global package
    global java_event_factory_event
    global java_event_factory_template

    outfile = '%s/EventFactory.java' % gen_path

    cases = ''
    for event_type, event_name, format, args in events:
        event_name = camel_case(event_name)
        cases += java_event_factory_event.format(event_type, event_name)
    subcases = ''
    for event_type, event_name, format, args in le_events:
        event_name = camel_case(event_name)
        subcases += java_event_factory_subevent.format(event_type, event_name)

    with open(outfile, 'w') as fout:
        defines_text = java_defines_string(defines)
        fout.write(java_event_factory_template.format(package, defines_text, cases, subcases))

def parse_events(path):
    global gen_path
    events = []
    le_events = []
    params = []
    event_types = set()
    format = None
    with open (path, 'rb') as fin:
        for line in fin:
            parts = re.match('.*@format\s*(\w*)\s*', line)
            if parts and len(parts.groups()) == 1:
                format = parts.groups()[0]
            parts = re.match('.*@param\s*(\w*)\s*', line)
            if parts and len(parts.groups()) == 1:
                param = parts.groups()[0]
                params.append(param)
            parts = re.match('\s*#define\s+(\w+)\s+(\w*)',line)
            if parts and len(parts.groups()) == 2:
                (key, value) = parts.groups()
                if format != None:
                    if key.lower().startswith('hci_subevent_'):
                        le_events.append((value, key.lower().replace('hci_subevent_', 'hci_event_'), format, params))
                    else:
                        events.append((value, key, format, params))
                    event_types.add(key)
                params = []
                format = None
    return (events, le_events, event_types)

# # read defines from hci_cmds.h and hci.h
read_defines(hci_cmds_h_path)
read_defines(hci_h_path)

# # parse commands and generate BTstack.java
parse_commands(hci_cmds_c_path)

# parse hci.h to get used events
(events, le_events, event_types) = parse_events(hci_cmds_h_path)

# create events, le meta events, and event factory
create_events(events)
create_events(le_events)
create_event_factory(events, le_events, event_types)

# done
print 'Done!'
