#!/usr/bin/env python2
# Script to handle build time requests embedded in C code.
#
# Copyright (C) 2016-2021  Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import sys, optparse, logging

FILEHEADER = """
/* DO NOT EDIT! This is an autogenerated file. See scripts/buildcommands.py. */

#include <stdint.h>
#include "board/irq.h"
#include "board/pgm.h"
#include "compiler.h"
#include "initial_pins.h"
"""

def error(msg):
    sys.stderr.write(msg + "\n")
    sys.exit(-1)

Handlers = []


######################################################################
# C call list generation
######################################################################

# Create dynamic C functions that call a list of other C functions
class HandleCallList:
    def __init__(self):
        self.call_lists = {'ctr_run_initfuncs': []}
        self.ctr_dispatch = { '_DECL_CALLLIST': self.decl_calllist }
    def decl_calllist(self, req):
        funcname, callname = req.split()[1:]
        self.call_lists.setdefault(funcname, []).append(callname)
    def update_data_dictionary(self, data):
        pass
    def generate_code(self, options):
        code = []
        for funcname, funcs in self.call_lists.items():
            func_code = ['    extern void %s(void);\n    %s();' % (f, f)
                         for f in funcs]
            if funcname == 'ctr_run_taskfuncs':
                add_poll = '    irq_poll();\n'
                func_code = [add_poll + fc for fc in func_code]
                func_code.append(add_poll)
            fmt = """
void
%s(void)
{
    %s
}
"""
            code.append(fmt % (funcname, "\n".join(func_code).strip()))
        return "".join(code)

Handlers.append(HandleCallList())


######################################################################
# Enumeration and static string generation
######################################################################

STATIC_STRING_MIN = 2

# Generate a dynamic string to integer mapping
class HandleEnumerations:
    def __init__(self):
        self.static_strings = []
        self.enumerations = {}
        self.ctr_dispatch = {
            'DECL_ENUMERATION': self.decl_enumeration,
            'DECL_ENUMERATION_RANGE': self.decl_enumeration_range
        }
    def add_enumeration(self, enum, name, value):
        enums = self.enumerations.setdefault(enum, {})
        if name in enums and enums[name] != value:
            error("Conflicting definition for enumeration '%s %s'" % (
                enum, name))
        enums[name] = value
    def decl_enumeration(self, req):
        enum, name, value = req.split()[1:]
        self.add_enumeration(enum, name, int(value, 0))
    def decl_enumeration_range(self, req):
        enum, name, value, count = req.split()[1:]
        self.add_enumeration(enum, name, (int(value, 0), int(count, 0)))
    def get_available_pins(self):
        avail_pins = {}
        pin_enums = self.enumerations.get('pin')
        if pin_enums is None:
            error("No pin enumeration available")
        for port_id, (offset, count) in pin_enums.items():
            for i in range(count):
                pin_name = "%s%d" % (port_id[:-1], i)
                pin_num = i + offset
                avail_pins[pin_name] = pin_num
        return avail_pins
    def generate_code(self, options):
        return ""

HandlerEnumerations = HandleEnumerations()
Handlers.append(HandlerEnumerations)


######################################################################
# Constants
######################################################################

# Allow adding build time constants to the data dictionary
class HandleConstants:
    def __init__(self):
        self.constants = {}
        self.ctr_dispatch = {
            'DECL_CONSTANT': self.decl_constant,
            'DECL_CONSTANT_STR': self.decl_constant_str,
        }
        self.reserved_pins = []
    def set_value(self, name, value):
        if name in self.constants and self.constants[name] != value:
            error("Conflicting definition for constant '%s'" % name)
        self.constants[name] = value
    def decl_constant(self, req):
        name, value = req.split()[1:]
        self.set_value(name, int(value, 0))
    def decl_constant_str(self, req):
        name, value = req.split(None, 2)[1:]
        value = value.strip()
        if value.startswith('"') and value.endswith('"'):
            value = value[1:-1]
        self.set_value(name, value)
    def get_reserved_pins(self):
        reserved_pins = []
        for name, val in self.constants.items():
            if name.upper().startswith("RESERVE_PINS"):
                rpins = [v.strip() for v in val.split(',') if v.strip()]
                reserved_pins.extend(rpins)
        return reserved_pins
    def lookup_pin(self, pin):
        avail_pins = HandlerEnumerations.get_available_pins()
        gpio = avail_pins.get(pin)
        if gpio is None:
            error("Pin %s is not available for this build" % (pin,))
        reserved_pins = self.get_reserved_pins()
        if pin in reserved_pins:
            error("Pin %s is reserved by an active MCU peripheral" % (pin,))
        return gpio
    def generate_code(self, options):
        return ""

HandlerConstants = HandleConstants()
Handlers.append(HandlerConstants)


######################################################################
# Initial pins
######################################################################

class HandleInitialPins:
    def __init__(self):
        self.initial_pins = []
        self.ctr_dispatch = { 'DECL_INITIAL_PINS': self.decl_initial_pins }
    def decl_initial_pins(self, req):
        pins = req.split(None, 1)[1].strip()
        if pins.startswith('"') and pins.endswith('"'):
            pins = pins[1:-1]
        if pins:
            self.initial_pins = [p.strip() for p in pins.split(',')]
            HandlerConstants.decl_constant_str(
                "_DECL_CONSTANT_STR INITIAL_PINS "
                + ','.join(self.initial_pins))
    def map_pins(self):
        if not self.initial_pins:
            return []
        out = []
        for p in self.initial_pins:
            flag = "IP_OUT_HIGH"
            if p.startswith('!'):
                flag = "0"
                p = p[1:].strip()
            gpio = HandlerConstants.lookup_pin(p)
            out.append("\n    {%d, %s}, // %s" % (gpio, flag, p))
        return out
    def generate_code(self, options):
        out = self.map_pins()
        fmt = """
const struct initial_pin_s initial_pins[] PROGMEM = {%s
};
const int initial_pins_size PROGMEM = ARRAY_SIZE(initial_pins);
"""
        return fmt % (''.join(out),)

Handlers.append(HandleInitialPins())


######################################################################
# ARM IRQ vector table generation
######################################################################

# Create ARM IRQ vector table from interrupt handler declarations
class Handle_arm_irq:
    def __init__(self):
        self.irqs = {}
        self.ctr_dispatch = { 'DECL_ARMCM_IRQ': self.decl_armcm_irq }
    def decl_armcm_irq(self, req):
        func, num = req.split()[1:]
        num = int(num, 0)
        if num in self.irqs and self.irqs[num] != func:
            error("Conflicting IRQ definition %d (old %s new %s)"
                  % (num, self.irqs[num], func))
        self.irqs[num] = func
    def generate_code(self, options):
        armcm_offset = 16
        if 1 - armcm_offset not in self.irqs:
            # The ResetHandler was not defined - don't build VectorTable
            return ""
        max_irq = max(self.irqs.keys())
        table = ["    DefaultHandler,\n"] * (max_irq + armcm_offset + 1)
        defs = []
        for num, func in self.irqs.items():
            if num < 1 - armcm_offset:
                error("Invalid IRQ %d (%s)" % (num, func))
            defs.append("extern void %s(void);\n" % (func,))
            table[num + armcm_offset] = "    %s,\n" % (func,)
        table[0] = "    &_stack_end,\n"
        fmt = """
extern void DefaultHandler(void);
extern uint32_t _stack_end;
%s
const void *VectorTable[] __visible __section(".vector_table") = {
%s};
"""
        return fmt % (''.join(defs), ''.join(table))

Handlers.append(Handle_arm_irq())


######################################################################
# Status LED Functionality
######################################################################

class HandleStatusLED:
    def __init__(self):
        self.pin = None
        self.ctr_dispatch = { 'DECL_LED_PIN': self.decl_led_pin }
    def decl_led_pin(self, req):
        pin = req.split(None, 1)[1].strip()
        if pin.startswith('"') and pin.endswith('"'):
            pin = pin[1:-1].strip()
        self.pin = pin
    def generate_code(self, options):
        led_gpio = led_gpio_high = 0
        pin = self.pin
        if pin:
            led_gpio_high = 1
            if pin[0] == "!":
                led_gpio_high = 0
                pin = pin[1:].strip()
            led_gpio = HandlerConstants.lookup_pin(pin)
        fmt = """
uint32_t led_gpio = %d, led_gpio_high = %d; // "%s"
"""
        return fmt % (led_gpio, led_gpio_high, self.pin)

Handlers.append(HandleStatusLED())


######################################################################
# Button entry functionality
######################################################################

class HandleButton:
    def __init__(self):
        self.pin = None
        self.ctr_dispatch = { 'DECL_BUTTON': self.decl_button }
    def decl_button(self, req):
        pin = req.split(None, 1)[1].strip()
        if pin.startswith('"') and pin.endswith('"'):
            pin = pin[1:-1].strip()
        self.pin = pin
    def generate_code(self, options):
        button_gpio = button_high = button_pullup = 0
        pin = self.pin
        if pin:
            if pin[0] in "^~":
                button_pullup = 1
                if pin[0] == "~":
                    button_pullup = -1
                pin = pin[1:].strip()
            button_high = 1
            if pin[0] == "!":
                button_high = 0
                pin = pin[1:].strip()
            button_gpio = HandlerConstants.lookup_pin(pin)
        fmt = """
int32_t button_gpio = %d, button_high = %d, button_pullup = %d; // "%s"
"""
        return fmt % (button_gpio, button_high, button_pullup, self.pin)

Handlers.append(HandleButton())


######################################################################
# Main code
######################################################################

def main():
    usage = "%prog [options] <cmd section file> <output.c>"
    opts = optparse.OptionParser(usage)
    opts.add_option("-v", action="store_true", dest="verbose",
                    help="enable debug messages")

    options, args = opts.parse_args()
    if len(args) != 2:
        opts.error("Incorrect arguments")
    incmdfile, outcfile = args
    if options.verbose:
        logging.basicConfig(level=logging.DEBUG)

    # Parse request file
    ctr_dispatch = { k: v for h in Handlers for k, v in h.ctr_dispatch.items() }
    f = open(incmdfile, 'r')
    data = f.read()
    f.close()
    for req in data.split('\n'):
        req = req.lstrip()
        if not req:
            continue
        cmd = req.split()[0]
        if cmd not in ctr_dispatch:
            error("Unknown build time command '%s'" % cmd)
        ctr_dispatch[cmd](req)

    # Write output
    code = "".join([FILEHEADER] + [h.generate_code(options) for h in Handlers])
    f = open(outcfile, 'w')
    f.write(code)
    f.close()

if __name__ == '__main__':
    main()
