import re

def slugify(value):
    value = re.sub(r'[^\w]+', '_', value.lower()).strip('_')
    if value[:1].isdigit():
        value = '_' + value
    return value

# A single variable (enclosed by the template)
class Var(object):
    STRUCT_NAME = 'vars'
    CSIZES = {
        # This array doesn't have to be exact, it's only used for sorting
        # struct members to save a few bytes of memory here and there
        'int':      4,
        'unsigned': 4,
        'float':    4,
        'ident_t':  2,
        'uint8_t':  1,
        'bool':     1,
    }

    def __init__(self, ctype, expr, name, csize=0, linenr=0):
        self.ctype  = ctype
        self.csize  = csize or Var.CSIZES[ctype]
        self.expr   = expr
        self.name   = name
        self.linenr = linenr

    def __str__(self):
        return f'{Var.STRUCT_NAME}.{self.name}'

def is_literal(expr):
    return expr.isnumeric() or expr in ['true', 'false']

# A (deduplicated) set of variables
class VarSet(object):
    def __init__(self):
        self.varmap = {} # expr -> cvar

    def __iter__(self):
        # Sort from largest to smallest variable to optimize struct padding
        yield from sorted(self.varmap.values(),
            reverse=True,
            key=lambda v: v.csize,
        )

    def __bool__(self):
        return True if self.varmap else False

    def add_var_raw(self, var):
        # Re-use existing entry for identical expression/type pairs
        if old := self.varmap.get(var.expr):
            if var.ctype != old.ctype:
                raise SyntaxError(f'Conflicting types for expression {var.expr}, '
                                  f'got {var.ctype}, expected {old.ctype}')
            assert old.name == var.name
            return old

        names = [ v.name for v in self.varmap.values() ]
        while var.name in names:
            var.name += '_'
        self.varmap[var.expr] = var
        return var

    # Returns the added variable
    def add_var(self, ctype, expr, name=None, linenr=0):
        assert expr
        expr = expr.strip()
        if is_literal(expr):
            return expr
        name = name or slugify(expr)

        var = Var(ctype, expr=expr, name=name, linenr=linenr)
        return self.add_var_raw(var)

    def merge(self, other):
        for var in other:
            self.add_var_raw(var)
