#!/usr/bin/env python3
# pylint: disable=invalid-name,missing-docstring
#
# Copyright 2023 Richard Hughes <richard@hughsie.com>
#
# SPDX-License-Identifier: LGPL-2.1-or-later

import os
import sys
import uuid
import argparse

from enum import Enum
from typing import Optional, List, Tuple, Dict

from jinja2 import Environment, FileSystemLoader, select_autoescape


class Endian(Enum):
    NATIVE = "native"
    LITTLE = "le"
    BIG = "be"


class Type(Enum):
    NONE = None
    U8 = "u8"
    U16 = "u16"
    U24 = "u24"
    U32 = "u32"
    U64 = "u64"
    STRING = "char"
    GUID = "Guid"
    B32 = "b32"
    I8 = "i8"
    I16 = "i16"
    I32 = "i32"
    I64 = "i64"


class Export(Enum):
    NONE = "none"
    PRIVATE = "static "
    PUBLIC = ""


# convert a CamelCase name into snake_case
def _camel_to_snake(name: str) -> str:
    # specified as all caps
    if name.upper() == name:
        return name.lower()

    name_snake: str = ""
    for char in name:
        if char.islower() or char.isnumeric():
            name_snake += char
            continue
        if char == "_":
            name_snake += char
            continue
        if name_snake:
            name_snake += "_"
        name_snake += char.lower()
    return name_snake


class EnumObj:
    def __init__(self, name: str) -> None:
        self.name: str = name
        self._since: Optional[str] = None
        self.comments: list[str] = []
        self.repr_type: Optional[str] = None
        self.items: List[EnumItem] = []
        self.is_imported: bool = False
        self._exports: Dict[str, Export] = {
            "ToString": Export.NONE,
            "FromString": Export.NONE,
        }
        self._c_methods: Dict[str, Export] = {}
        self._derives_since: Dict[str, str] = {}
        self._is_bitfield = False
        self._is_force_enum = False

    def c_method(self, suffix: str):

        # override
        if suffix in self._c_methods:
            return self._c_methods[suffix]

        name_snake = _camel_to_snake(self.name)
        if self._is_force_enum:
            if name_snake.endswith("flags"):
                name_snake = name_snake[:-1]

        return f"{name_snake}_{_camel_to_snake(suffix)}"

    def since(self, derive: str) -> Optional[str]:
        if derive in self._derives_since:
            return self._derives_since[derive]
        return self._since

    @property
    def c_type(self):
        return f"{self.name}"

    @property
    def c_define_last(self) -> str:
        return f"{_camel_to_snake(self.name).upper()}_LAST"

    @property
    def items_any_defaults(self) -> bool:
        for item in self.items:
            if item.default:
                return True
        return False

    @property
    def is_bitfield(self) -> bool:
        if self._is_force_enum:
            return False
        for item in self.items:
            if item.is_bitfield:
                return True
        return self._is_bitfield

    def check(self, prefix: Optional[str] = None):
        # check we're prefixed with something sane
        if prefix and not self.name.startswith(prefix):
            raise ValueError(f"enum {self.name} does not have '{prefix}' prefix")

        # check we'd not just done ZERO=0, ONE=1, TWO=2, etc
        indexed = True
        for i, item in enumerate(self.items):
            if str(i) != item.default:
                indexed = False
                break
        if indexed:
            raise ValueError(f"enum {self.name} does not need explicit defaults")

        # check each enum
        for item in self.items:
            item.check()

    def item(self, name: str) -> Optional["EnumItem"]:
        for item in self.items:
            if item.name == name:
                return item
        return None

    def add_private_export(self, derive: str) -> None:
        if self._exports[derive] == Export.PUBLIC:
            return
        self._exports[derive] = Export.PRIVATE

    def add_export_param(self, derive: str, value: str):

        if derive in ["FromString", "ToString"] and value == "enum":
            self._is_force_enum = True
            return
        if value.startswith("since="):
            self._derives_since[derive] = value[6:]
            return
        if value.startswith("name="):
            self._c_methods[derive] = value[5:]
            return
        raise ValueError(f"derive {derive} parameter {value} unknown")

    def add_public_export(self, derive: str) -> None:

        # split out the any derive params
        idx = derive.find("(")
        if idx != -1:
            params = derive[idx + 1 : -1].split(";")
            derive = derive[:idx]
            for param in params:
                self.add_export_param(derive, param)

        if derive == "Bitfield":
            self._is_bitfield = True
            return
        self.add_private_export(derive)
        self._exports[derive] = Export.PUBLIC

    def export(self, derive: str) -> Export:
        return self._exports[derive]

    def __str__(self) -> str:
        return f"EnumObj({self.name})"


class EnumItem:
    def __init__(self, obj: EnumObj) -> None:
        self.obj: EnumObj = obj
        self.name: str = ""
        self.default: Optional[str] = None
        self.comments: list[str] = []
        self.since: Optional[str] = None
        self.is_bitfield = False

    @property
    def c_define(self) -> str:
        name_snake = _camel_to_snake(self.obj.name)
        if name_snake.endswith("flags") or name_snake.endswith("attrs"):
            name_snake = name_snake[:-1]
        return f"{name_snake.upper()}_{_camel_to_snake(self.name).replace('-', '_').upper()}"

    def parse_default(self, val: str) -> None:
        val = {
            "u64::MAX": "G_MAXUINT64",
            "u32::MAX": "G_MAXUINT32",
            "u16::MAX": "G_MAXUINT16",
            "u8::MAX": "G_MAXUINT8",
        }.get(val, val)

        # parse bitfield shifts
        try:
            number, bitshift = val.split("<<", maxsplit=1)
        except ValueError:
            pass
        else:
            self.is_bitfield = True
            # make sure we promote to a larger integer type
            if int(bitshift) >= 31:
                val = f"{int(number)}ull<<{bitshift}"
        if val.startswith("0x") or val.startswith("0b"):
            val = val.replace("_", "")
        if val.startswith("0b"):
            val = hex(int(val[2:], 2))
        self.default = val

    def check(self):
        uppercase_cnt: int = 0
        for char in self.name:
            if char.isupper():
                if uppercase_cnt > 1:
                    raise ValueError(
                        f"enum {self.name} had too many consecutive uppercase chars"
                    )
                uppercase_cnt += 1
            else:
                uppercase_cnt = 0

    @property
    def value(self) -> str:
        return _camel_to_snake(self.name).replace("_", "-")

    def __str__(self) -> str:
        return f"EnumItem({self.name}={self.default})"


class StructObj:
    def __init__(self, name: str) -> None:
        self.name: str = name
        self.items: List[StructItem] = []
        self.is_imported: bool = False
        self._exports: Dict[str, Export] = {
            "Validate": Export.NONE,
            "ValidateBytes": Export.NONE,
            "ValidateStream": Export.NONE,
            "ValidateInternal": Export.NONE,
            "Parse": Export.NONE,
            "ParseBytes": Export.NONE,
            "ParseStream": Export.NONE,
            "ParseInternal": Export.NONE,
            "New": Export.NONE,
            "NewInternal": Export.NONE,
            "ToString": Export.NONE,
            "ToBytes": Export.NONE,
            "Default": Export.NONE,
        }

    def c_method(self, suffix: str):
        return f"{_camel_to_snake(self.name)}_{_camel_to_snake(suffix)}"

    def c_define(self, suffix: str):
        return f"{_camel_to_snake(self.name).upper()}_{suffix.upper()}"

    @property
    def _has_bits(self) -> bool:
        for item in self.items:
            if item.type == Type.B32:
                return True
        return False

    @property
    def size(self) -> int:
        size: int = 0
        if self._has_bits:
            return 4
        for item in self.items:
            size += item.size
        return size

    @property
    def has_constant(self) -> bool:
        for item in self.items:
            if item.constant:
                return True
        return False

    def check(self, prefix: Optional[str] = None):
        # check we're prefixed with something sane
        if prefix and not self.name.startswith(prefix):
            raise ValueError(f"struct {self.name} does not have '{prefix}' prefix")

    def add_private_export(self, derive: str) -> None:
        if self._exports[derive] == Export.PUBLIC:
            return
        self._exports[derive] = Export.PRIVATE
        if derive == "Validate":
            self.add_private_export("ValidateInternal")
        elif derive == "ValidateStream":
            self.add_private_export("NewInternal")
            self.add_private_export("ValidateInternal")
        elif derive == "ValidateBytes":
            self.add_private_export("Validate")
        elif derive == "ValidateInternal":
            for item in self.items:
                if item.constant and not (item.type == Type.U8 and item.n_elements):
                    item.add_private_export("Getters")
                if item.constant and item.enum_obj:
                    item.enum_obj.add_private_export("ToString")
                if item.struct_obj:
                    item.struct_obj.add_private_export("ValidateInternal")
        elif derive == "ToString":
            for item in self.items:
                if item.struct_obj:
                    item.struct_obj.add_private_export("ToString")
                elif item.enum_obj and item.enabled:
                    item.enum_obj.add_private_export("ToString")
                elif item.enabled:
                    item.add_private_export("Getters")
        elif derive == "Parse":
            self.add_private_export("NewInternal")
            self.add_private_export("ParseInternal")
        elif derive == "ParseStream":
            self.add_private_export("NewInternal")
            self.add_private_export("ParseInternal")
        elif derive == "ParseBytes":
            self.add_private_export("Parse")
        elif derive == "ParseInternal":
            self.add_private_export("ToString")
            self.add_private_export("ValidateInternal")
            for item in self.items:
                if item.struct_obj:
                    item.struct_obj.add_private_export("ValidateInternal")
        elif derive == "New":
            self.add_private_export("NewInternal")
            for item in self.items:
                if item.constant and not (item.type == Type.U8 and item.n_elements):
                    item.add_private_export("Setters")
                if item.struct_obj:
                    item.struct_obj.add_private_export("New")

    def add_public_export(self, derive: str) -> None:
        # Getters and Setters are special as we do not want public exports of const
        if derive in ["Getters", "Setters"]:
            for item in self.items:
                if not item.constant:
                    item.add_public_export(derive)
                if item.struct_obj:
                    item.struct_obj.add_private_export("NewInternal")
        else:
            self.add_private_export(derive)
            self._exports[derive] = Export.PUBLIC

        # for convenience
        if derive in ["Parse", "ParseBytes", "ParseStream"]:
            self.add_public_export("Getters")
            for item in self.items:
                if item.struct_obj:
                    item.struct_obj.add_public_export("Getters")
        if derive == "New":
            self.add_public_export("Setters")

    def export(self, derive: str) -> Export:
        return self._exports[derive]

    def __str__(self) -> str:
        return f"StructObj({self.name})"


class StructItem:
    def __init__(self, obj: StructObj) -> None:
        self.obj: StructObj = obj
        self.element_id: str = ""
        self.type: Type = Type.NONE
        self.is_packed: bool = False
        self.enum_obj: Optional[EnumObj] = None
        self.struct_obj: Optional[StructObj] = None
        self.default: Optional[str] = None
        self.constant: Optional[str] = None
        self.padding: Optional[str] = None
        self.endian: Endian = Endian.NATIVE
        self.n_elements: int = 0
        self._bits_size: int = 0
        self._bits_offset: int = 0
        self.offset: int = 0
        self._exports: Dict[str, Export] = {
            "Getters": Export.NONE,
            "Setters": Export.NONE,
        }

    def add_private_export(self, derive: str) -> None:
        if self._exports[derive] == Export.PUBLIC:
            return
        self._exports[derive] = Export.PRIVATE

    def add_public_export(self, derive: str) -> None:
        self.add_private_export(derive)
        self._exports[derive] = Export.PUBLIC

    def export(self, derive: str) -> Export:
        return self._exports[derive]

    @property
    def bits_offset(self) -> int:
        # from 32 bit word start
        return self._bits_offset

    @property
    def bits_size(self) -> int:
        if self.type == Type.B32:
            return self._bits_size
        return self.size * 8

    @property
    def bits_mask(self) -> int:
        return (1 << self._bits_size) - 1

    @property
    def size(self) -> int:
        n_elements = self.n_elements
        if not n_elements:
            n_elements = 1
        if self.struct_obj:
            return n_elements * self.struct_obj.size
        if self.type in [Type.U8, Type.I8, Type.STRING]:
            return n_elements
        if self.type in [Type.GUID]:
            return n_elements * 16
        if self.type in [Type.U16, Type.I16]:
            return n_elements * 2
        if self.type == Type.U24:
            return n_elements * 3
        if self.type in [Type.U32, Type.I32]:
            return n_elements * 4
        if self.type in [Type.U64, Type.I64]:
            return n_elements * 8
        return 0

    @property
    def enabled(self) -> bool:
        if self.element_id.startswith("_"):
            return False
        if self.element_id == "reserved":
            return False
        return True

    @property
    def endian_glib(self) -> str:
        if self.endian == Endian.LITTLE:
            return "G_LITTLE_ENDIAN"
        if self.endian == Endian.BIG:
            return "G_BIG_ENDIAN"
        return "G_BYTE_ORDER"

    def c_define(self, suffix: str):
        return self.obj.c_define(suffix.upper() + "_" + self.element_id.upper())

    @property
    def c_getter(self):
        return self.obj.c_method("get_" + self.element_id)

    @property
    def c_setter(self):
        return self.obj.c_method("set_" + self.element_id)

    @property
    def type_glib(self) -> str:
        if self.enum_obj:
            return self.enum_obj.c_type
        if self.type == Type.U8:
            return "guint8"
        if self.type == Type.U16:
            return "guint16"
        if self.type == Type.U24:
            return "guint32"
        if self.type == Type.U32:
            return "guint32"
        if self.type == Type.U64:
            return "guint64"
        if self.type == Type.STRING:
            return "gchar"
        if self.type == Type.GUID:
            return "fwupd_guid_t"
        if self.type == Type.B32:
            return "guint32"
        if self.type == Type.I8:
            return "gint8"
        if self.type == Type.I16:
            return "gint16"
        if self.type == Type.I32:
            return "gint32"
        if self.type == Type.I64:
            return "gint64"
        return "void"

    @property
    def type_mem(self) -> str:
        if self.type == Type.U16:
            return "uint16"
        if self.type == Type.U24:
            return "uint24"
        if self.type == Type.U32:
            return "uint32"
        if self.type == Type.B32:
            return "uint32"
        if self.type == Type.U64:
            return "uint64"
        if self.type == Type.I16:
            return "uint16"
        if self.type == Type.I32:
            return "uint32"
        if self.type == Type.I64:
            return "uint64"
        return ""

    def _parse_default(self, val: str) -> str:
        if self.enum_obj:
            enum_item = self.enum_obj.item(val)
            if not enum_item:
                msg: str = [item.name for item in self.enum_obj.items]
                raise ValueError(f"enum default unknown, got {val} expected: {msg}")
            return enum_item.c_define
        if self.type == Type.STRING:
            if val.startswith('"') and val.endswith('"'):
                return val[1:-1]
            raise ValueError(f"string default {val} needs double quotes")
        if self.type == Type.GUID:
            if val.startswith("0x"):
                guid = uuid.UUID(bytes_le=bytes.fromhex(val[2:]))
                raise ValueError(f"integer {val} expected, expected: {guid}")
            if not val.startswith('"'):
                raise ValueError(f"string expected, got: {val}")
            uuid2 = uuid.UUID(val[1:-1])
            val_hex = ""
            for value in uuid2.bytes_le:
                val_hex += f"\\x{value:x}"
            return val_hex
        if self.type == Type.U8 and self.n_elements:
            val_hex = ""
            if val.startswith("[") and val.endswith("]"):
                value, n_elements = val[1:-1].split(";", maxsplit=1)
                if not value.startswith("0x"):
                    raise ValueError(f"0x prefix for hex number expected, got: {val}")
                if self.size != int(n_elements):
                    raise ValueError(
                        f"data has to be {self.size} bytes exactly. Is {n_elements}"
                    )
                for _ in range(int(n_elements)):
                    val_hex += f"\\x{value[2:]}"
                return val_hex

            if not val.startswith("0x"):
                raise ValueError(f"0x prefix for hex number expected, got: {val}")
            if len(val) != (self.size * 2) + 2:
                raise ValueError(f"data has to be {self.size} bytes exactly")
            for idx in range(2, len(val), 2):
                val_hex += f"\\x{val[idx:idx+2]}"
            return val_hex
        if self.type in [
            Type.U8,
            Type.U16,
            Type.U24,
            Type.U32,
            Type.U64,
            Type.B32,
        ]:
            if val.startswith("0x") or val.startswith("0b"):
                val = val.replace("_", "")
            return val.replace("$struct_offset", str(self.offset))
        raise ValueError(f"do not know how to parse value for type: {self.type}")

    def parse_default(self, val: str) -> None:
        self.default = self._parse_default(val)

    def parse_constant(self, val: str) -> None:
        self.default = self._parse_default(val)
        self.constant = self.default

    def parse_type(
        self, val: str, enum_objs: Dict[str, EnumObj], struct_objs: Dict[str, StructObj]
    ) -> None:
        # is array
        if val.startswith("[") and val.endswith("]"):
            typestr, n_elements = val[1:-1].split(";", maxsplit=1)
            n_elements = n_elements.strip()
            if n_elements.startswith("0x"):
                self.n_elements = int(n_elements[2:], 16)
            else:
                self.n_elements = int(n_elements)
        else:
            typestr = val

        # nested struct
        if typestr in struct_objs:
            self.struct_obj = struct_objs[typestr]
            return

        # find the type
        if typestr in enum_objs:
            self.enum_obj = enum_objs[typestr]
            typestr_maybe: Optional[str] = enum_objs[typestr].repr_type
            if not typestr_maybe:
                raise ValueError(f"no repr for: {typestr}")
            typestr = typestr_maybe

        # detect endian
        if typestr.endswith("be"):
            self.endian = Endian.BIG
            typestr = typestr[:-2]
        elif typestr.endswith("le"):
            self.endian = Endian.LITTLE
            typestr = typestr[:-2]

        # support partial bytes
        for bits_size in range(1, 32):
            if bits_size in [8, 16, 24, 32]:
                continue
            if typestr == f"u{bits_size}":
                self.type = Type.B32
                self._bits_size = bits_size
                if self.endian == Endian.NATIVE:
                    self.endian = Endian.LITTLE
                return

        # defined types
        try:
            self.type = Type(typestr)
        except ValueError as e:
            raise ValueError(f"invalid type: {typestr}") from e

        # sanity check
        if (
            self.enabled
            and self.is_packed
            and self.endian == Endian.NATIVE
            and self.type
            in [Type.U16, Type.U24, Type.U32, Type.U64, Type.I16, Type.I32, Type.I64]
        ):
            raise ValueError(f"endian not specified for packed struct: {typestr}")

    def __str__(self) -> str:
        tmp = f"{self.element_id}: "
        if self.n_elements:
            tmp += str(self.n_elements)
        tmp += self.type.value
        if self.endian != Endian.NATIVE:
            tmp += self.endian.value
        if self.default:
            tmp += f" = {self.default}"
        elif self.constant:
            tmp += f" == {self.constant}"
        elif self.padding:
            tmp += f" = {self.padding}"
        return tmp


class Generator:
    def __init__(
        self,
        basename,
        modules_map: Dict[str, str],
        prefix: Optional[str] = None,
        includes=[],
    ) -> None:
        self.basename: str = basename
        self.prefix: Optional[str] = prefix
        self.import_headers: list[str] = []
        self.modules_map: Dict[str, str] = modules_map
        self.includes: list[str] = includes
        self.struct_objs: Dict[str, StructObj] = {}
        self.enum_objs: Dict[str, EnumObj] = {}
        self._env = Environment(
            loader=FileSystemLoader(os.path.dirname(__file__)),
            autoescape=select_autoescape(),
            keep_trailing_newline=True,
        )

    def _process_enums(self, enum_obj: EnumObj) -> Tuple[str, str]:
        # render
        subst = {
            "Type": Type,
            "Export": Export,
            "obj": enum_obj,
        }
        template_h = self._env.get_template(os.path.basename("fu-rustgen-enum.h.in"))
        template_c = self._env.get_template(os.path.basename("fu-rustgen-enum.c.in"))
        return template_c.render(subst), template_h.render(subst)

    def _process_structs(self, struct_obj: StructObj) -> Tuple[str, str]:
        # render
        subst = {
            "Type": Type,
            "Export": Export,
            "obj": struct_obj,
        }
        template_h = self._env.get_template(os.path.basename("fu-rustgen-struct.h.in"))
        template_c = self._env.get_template(os.path.basename("fu-rustgen-struct.c.in"))
        return template_c.render(subst), template_h.render(subst)

    def _use_import(self, where: str, module: str, what: str) -> None:

        module_basename = module.replace("_", "-")
        try:
            fn = os.path.join(self.modules_map[where], f"fu-{module_basename}.rs")
        except KeyError:
            raise ValueError(f"invalid module name: {where}")
        child = Generator(self.basename, self.modules_map, prefix=self.prefix)
        with open(fn, "rb") as f:
            child._parse_input(f.read().decode())

        # header includes
        header_basename: str = f"fu-{module_basename}-struct.h"
        if header_basename not in self.import_headers:
            self.import_headers.append(header_basename)

        # is enum
        if what in child.enum_objs:
            enum_obj = child.enum_objs[what]
            enum_obj.is_imported = True
            self.enum_objs[what] = enum_obj
            return

        # is struct
        if what in child.struct_objs:
            struct_obj = child.struct_objs[what]
            struct_obj.is_imported = True
            self.struct_objs[what] = struct_obj
            return

        # not found
        raise ValueError(f"invalid struct or enum name: {what}")

    def _parse_input(self, contents: str) -> None:
        name = None
        repr_type: Optional[str] = None
        derives: List[str] = []
        offset: int = 0
        struct_seen_b32: bool = False
        bits_offset: int = 0
        since: Optional[str] = None
        struct_cur: Optional[StructObj] = None
        enum_cur: Optional[EnumObj] = None
        comments_cur: list[str] = []

        for line_num, line in enumerate(contents.split("\n")):
            # replace all tabs with spaces
            line = line.replace("\t", "  ")

            # import one file into another
            if line.startswith("use "):
                if not line.endswith(";"):
                    raise ValueError(f"use requires a semicolon on line {line_num}")
                where, why, what = line[4:-1].split("::", maxsplit=3)
                self._use_import(where, why, what)

            # remove comments and indent
            try:
                line, comment = line.split("//", maxsplit=1)
            except ValueError:
                pass
            else:
                comment = comment.strip()
                if comment.startswith("Since:"):
                    since = comment[6:].strip()
                elif comment.startswith("SPDX") or comment.startswith("Copyright"):
                    pass
                elif comment:
                    comments_cur.append(comment.strip())
            line = line.strip()
            if not line:
                continue

            # start of structure
            if line.startswith("struct ") and line.endswith("{"):
                name = line[6:-1].strip()
                if name in self.struct_objs:
                    raise ValueError(
                        f"struct {name} already defined on line {line_num}"
                    )
                struct_cur = StructObj(name)
                self.struct_objs[name] = struct_cur
                continue
            if line.startswith("enum ") and line.endswith("{"):
                name = line[4:-1].strip()
                if name in self.enum_objs:
                    raise ValueError(f"enum {name} already defined on line {line_num}")
                enum_cur = EnumObj(name)
                enum_cur.repr_type = repr_type
                enum_cur._since = since
                enum_cur.comments.extend(comments_cur)
                self.enum_objs[name] = enum_cur
                comments_cur.clear()
                continue

            # the enum type
            if line.startswith("#[repr(") and line.endswith(")]"):
                repr_type = line[7:-2]
                continue

            # what should we build
            if line.startswith("#[derive("):
                for derive in line[9:-2].split(","):
                    derives.append(derive.strip())
                continue

            # not in object
            if not struct_cur and not enum_cur:
                continue

            # end of structure
            if line.startswith("}"):
                if struct_cur:
                    struct_cur.check(prefix=self.prefix)
                    for derive in derives:
                        struct_cur.add_public_export(derive)
                    for item in struct_cur.items:
                        if item.default == "$struct_size":
                            item.default = str(offset)
                        if item.constant == "$struct_size":
                            item.constant = str(offset)
                if enum_cur:
                    enum_cur.check(prefix=self.prefix)
                    for derive in derives:
                        enum_cur.add_public_export(derive)
                struct_cur = None
                enum_cur = None
                repr_type = None
                comments_cur.clear()
                since = None
                derives.clear()
                offset = 0
                bits_offset = 0
                struct_seen_b32 = False
                continue

            # check for trailing comma
            if not line.endswith(","):
                raise ValueError(
                    f"invalid struct line on line {line_num}: {line} -- needs trailing comma"
                )
            line = line[:-1]

            # split enumeration into sections
            if enum_cur:
                enum_item = EnumItem(enum_cur)
                enum_item._since = since
                enum_item.comments.extend(comments_cur)
                parts = line.split("=", maxsplit=2)
                enum_item.name = parts[0].strip()
                if len(parts) > 1:
                    enum_item.parse_default(parts[1].strip())
                enum_cur.items.append(enum_item)
                comments_cur.clear()

            # split structure into sections
            if struct_cur:
                # parse "signature: u32be == 0x12345678"
                parts = line.split(":", maxsplit=2)
                if len(parts) == 1:
                    raise ValueError(f"invalid struct line on line {line_num}: {line}")

                # parse one element
                item = StructItem(struct_cur)
                item._bits_offset = bits_offset
                item.offset = offset
                item.element_id = parts[0].strip()
                if repr_type == "C, packed":
                    item.is_packed = True

                type_parts = parts[1].split("=", maxsplit=3)
                try:
                    item.parse_type(
                        type_parts[0].strip(),
                        enum_objs=self.enum_objs,
                        struct_objs=self.struct_objs,
                    )
                except ValueError as e:
                    raise ValueError(f"{str(e)} on line {line_num}: {line}")
                if len(type_parts) > 1:
                    if "Default" not in derives:
                        raise ValueError(
                            f"struct requires #[derive(Default)] for line {line_num}: {line}"
                        )
                if len(type_parts) == 3:
                    item.parse_constant(type_parts[2].strip())
                elif len(type_parts) == 2:
                    item.parse_default(type_parts[1].strip())
                if item.size == 0:
                    struct_seen_b32 = True
                if not struct_seen_b32:
                    offset += item.size
                bits_offset += item.bits_size
                struct_cur.items.append(item)

    def process_input(self, contents: str) -> Tuple[str, str]:

        # parse input
        self._parse_input(contents)

        # process the templates here
        subst = {
            "basename": self.basename,
            "enum_objs": self.enum_objs,
            "struct_objs": self.struct_objs,
            "import_headers": self.import_headers,
            "includes": self.includes,
        }
        template_h = self._env.get_template(os.path.basename("fu-rustgen.h.in"))
        template_c = self._env.get_template(os.path.basename("fu-rustgen.c.in"))
        dst_h = template_h.render(subst)
        dst_c = template_c.render(subst)
        for enum_obj in self.enum_objs.values():
            if enum_obj.is_imported:
                continue
            str_c, str_h = self._process_enums(enum_obj)
            dst_c += str_c
            dst_h += str_h
        for struct_obj in self.struct_objs.values():
            if struct_obj.is_imported:
                continue
            str_c, str_h = self._process_structs(struct_obj)
            dst_c += str_c
            dst_h += str_h

        # success
        return dst_c, dst_h


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("src", action="store", type=str, help="source")
    parser.add_argument("--outc", action="store", type=str, help="destination .c")
    parser.add_argument("--outh", action="store", type=str, help="destination .h")
    parser.add_argument("--prefix", action="store", type=str, default="", help="prefix")
    parser.add_argument("--use", action="append", default=[], help="module:path")
    parser.add_argument(
        "--include", action="append", default=[], help="fwupd.h|fwupdplugin.h"
    )
    args = parser.parse_args()

    # parse map from module to path
    modules_map: dict[str, str] = {}
    for entry in args.use:
        try:
            split = entry.split(":", maxsplit=1)
            modules_map[split[0]] = split[1]
        except IndexError:
            sys.exit(f"expected module:path, got {entry}")

    g = Generator(
        basename=os.path.basename(args.outh or args.outc.replace(".c", ".h")),
        modules_map=modules_map,
        includes=args.include,
        prefix=args.prefix,
    )
    with open(args.src, "rb") as f:
        try:
            dst_c, dst_h = g.process_input(
                f.read().decode(),
            )
        except ValueError as e:
            sys.exit(f"cannot process {args.src}: {str(e)}")
    if args.outc:
        with open(args.outc, "wb") as f:  # type: ignore
            f.write(dst_c.encode())
    if args.outh:
        with open(args.outh, "wb") as f:  # type: ignore
            f.write(dst_h.encode())
