#!/usr/bin/env python3
#
# Copyright (c) 2016-2024 Arkadiusz Bokowy
#
# This file is a part of bluez-alsa.
#
# This project is licensed under the terms of the MIT license.

import argparse
import os.path
import re
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET

TEMPLATE_HEADER_PREAMBLE = '''/*
 * This file is generated by dbus-codegen.py, do not modify it.
 *
 * The license of this code is the same as for the D-Bus interface description
 * it was derived from.
 */

#pragma once
#ifndef _{name}_
#define _{name}_

#include <gio/gio.h>
#include <glib.h>
#include "dbus.h"
'''

TEMPLATE_HEADER = '''
typedef struct {{
  GDBusInterfaceSkeletonEx parent;
}} {struct}Skeleton;

{struct}Skeleton *{func}_skeleton_new(
    const GDBusInterfaceSkeletonVTable *vtable,
    void *userdata,
    GDestroyNotify userdata_free_func);
'''

TEMPLATE_HEADER_CLOSING = '#endif'

TEMPLATE_BODY_PREAMBLE = '''/*
 * This file is generated by dbus-codegen.py, do not modify it.
 *
 * The license of this code is the same as for the D-Bus interface description
 * it was derived from.
 */

#include "{header}"
#include <gio/gio.h>
#include <glib-object.h>
#include <glib.h>
#include "dbus.h"
'''

TEMPLATE_BODY = '''
typedef struct {{
  GDBusInterfaceSkeletonClass parent;
}} {struct}SkeletonClass;

G_DEFINE_TYPE({struct}Skeleton, {func}_skeleton,
  G_TYPE_DBUS_INTERFACE_SKELETON);

static void {func}_skeleton_class_init({struct}SkeletonClass *ifc) {{
    GDBusInterfaceSkeletonClass *ifc_ = G_DBUS_INTERFACE_SKELETON_CLASS(ifc);
    ifc_->get_info = g_dbus_interface_skeleton_ex_class_get_info;
    ifc_->get_vtable = g_dbus_interface_skeleton_ex_class_get_vtable;
    ifc_->get_properties = g_dbus_interface_skeleton_ex_class_get_properties;
}}

static void {func}_skeleton_init(G_GNUC_UNUSED {struct}Skeleton *ifs) {{
}}

/**
 * Create a skeleton for {iface} interface.
 *
 * @return On success, this function returns newly allocated GIO interface
 *   skeleton object, which shall be freed with g_object_unref(). If error
 *   occurs, NULL is returned. */
{struct}Skeleton *{func}_skeleton_new(
    const GDBusInterfaceSkeletonVTable *vtable,
    void *userdata,
    GDestroyNotify userdata_free_func) {{
  const GType type = {func}_skeleton_get_type();
  return g_dbus_interface_skeleton_ex_new(type,
    (GDBusInterfaceInfo *)&{func}_interface,
    vtable, userdata, userdata_free_func);
}}
'''

parser = argparse.ArgumentParser()
parser.add_argument('--interface-info-header', action='store_true',
                    help='Generate GDBusInterfaceInfo header')
parser.add_argument('--interface-info-body', action='store_true',
                    help='Generate GDBusInterfaceInfo body')
parser.add_argument('--interface-skeleton-header', action='store_true',
                    help='Generate GDBusInterfaceSkeletonEx header')
parser.add_argument('--interface-skeleton-body', action='store_true',
                    help='Generate GDBusInterfaceSkeletonEx body')
parser.add_argument('-D', '--define', action='append', default=[],
                    metavar='NAME', help='Define preprocessor macro')
parser.add_argument('-a', '--append', action='store_true',
                    help='Append instead of overwriting the output file')
parser.add_argument('--output', type=str, metavar='FILE',
                    help='Write output into the specified file')
parser.add_argument('input', type=str, metavar='FILE',
                    help='D-Bus introspection XML file')

args = parser.parse_args()

# Check for "CPP.if" annotations and remove unwanted interfaces.
xml = ET.parse(args.input).getroot()
parents = {n: p for p in xml.iter() for n in p}
for ann in xml.findall('.//annotation[@name="org.gtk.GDBus.CPP.if"]'):
    if ann.get('value') not in args.define:
        node = parents[ann]  # get parent of annotation
        parents[node].remove(node)  # remove node from its parent

output_basename = os.path.basename(args.output)
output_header = os.path.splitext(output_basename)[0] + '.h'
output_ifdef = output_basename.upper().replace('.', '_').replace('-', '_')

# Generate interface info header and/or body with upstream tool.
if args.interface_info_header or args.interface_info_body:
    # Save modified XML tree into a temporary file for further processing
    # with upstream code generator.
    with tempfile.NamedTemporaryFile(mode='w') as f:
        ET.ElementTree(xml).write(f.name)
        f.flush()
        command = ['gdbus-codegen', '--output', args.output]
        if args.interface_info_header:
            command += ['--interface-info-header']
        if args.interface_info_body:
            command += ['--interface-info-body']
        try:
            subprocess.check_call(command + [f.name])
        except subprocess.CalledProcessError:
            sys.exit(1)
        args.append = True

with open(args.output, 'a' if args.append else 'w') as f:

    if args.interface_skeleton_header:
        f.write(TEMPLATE_HEADER_PREAMBLE.format(name=output_ifdef))
    if args.interface_skeleton_body:
        f.write(TEMPLATE_BODY_PREAMBLE.format(header=output_header))

    for iface in xml:

        iface = iface.attrib['name']
        iface_ = re.sub(r'([a-z])([A-Z])', r'\1.\2', iface)
        struct = iface_.title().replace('.', '')
        func = iface_.lower().replace('.', '_')

        if args.interface_skeleton_header:
            f.write(TEMPLATE_HEADER.format(struct=struct, func=func))
        if args.interface_skeleton_body:
            f.write(TEMPLATE_BODY.format(
                iface=iface, struct=struct, func=func))

    if args.interface_skeleton_header:
        f.write(TEMPLATE_HEADER_CLOSING)
