#!/usr/bin/env python3
# Copyright 2020-2023, Collabora, Ltd.
# SPDX-License-Identifier: BSL-1.0
"""Generate code from a JSON file describing interaction profiles and
bindings."""

import argparse
import json
import copy
from operator import attrgetter

def find_component_in_list_by_name(name, component_list, subaction_path=None, identifier_json_path=None):
    """Find a component with the given name in a list of components."""
    for component in component_list:
        if component.component_name == name:
            if subaction_path is not None and component.subaction_path != subaction_path:
                continue
            if identifier_json_path is not None and component.identifier_json_path != identifier_json_path:
                continue
            return component
    return None


def steamvr_subpath_name(steamvr_path, subpath_type):
    if subpath_type == "pose":
        return steamvr_path.replace("/input/", "/pose/")

    if subpath_type == "trigger" or subpath_type == "button":
        return steamvr_path.replace("squeeze", "grip")

    if subpath_type == "joystick":
        return steamvr_path.replace("thumbstick", "joystick")

    return steamvr_path


class PathsByLengthCollector:
    """Helper class to sort paths by length, useful for creating fast path
    validation functions.
    """

    def __init__(self):
        self.by_length = dict()

    def add_path(self, path):
        length = len(path)
        if length in self.by_length:
            self.by_length[length].add(path)
        else:
            self.by_length[length] = {path}

    def add_paths(self, paths):
        for path in paths:
            self.add_path(path)

    def to_dict_of_lists(self):
        ret = dict()
        for length, set_per_length in self.by_length.items():
            ret[length] = list(set_per_length)
        return ret


def dpad_paths(identifier_path, center):
    paths = [
        identifier_path + "/dpad_up",
        identifier_path + "/dpad_down",
        identifier_path + "/dpad_left",
        identifier_path + "/dpad_right",
    ]

    if center:
        paths.append(identifier_path + "/dpad_center")

    return paths


class DPad:
    """Class holding per identifier information for dpad emulation."""

    @classmethod
    def parse_dpad(dpad_cls,
                   identifier_path,
                   component_list,
                   dpad_json):
        center = dpad_json["center"]
        position_str = dpad_json["position"]
        activate_str = dpad_json.get("activate")

        position_component = find_component_in_list_by_name(position_str,
                                                            component_list)
        activate_component = find_component_in_list_by_name(activate_str,
                                                            component_list)

        paths = dpad_paths(identifier_path, center)

        return DPad(center,
                    paths,
                    position_component,
                    activate_component)

    def __init__(self,
                 center,
                 paths,
                 position_component,
                 activate_component):
        self.center = center
        self.paths = paths
        self.position_component = position_component
        self.activate_component = activate_component


class Component:
    """Components correspond with the standard OpenXR components click, touch,
    force, value, x, y, twist, pose
    """

    @classmethod
    def parse_components(component_cls,
                         subaction_path,
                         identifier_json_path,
                         json_subpath):
        """
        Turn a Identifier's component paths into a list of Component objects.
        """

        component_list = []
        for component_name in json_subpath["components"]:  # click, touch, ...
            matched_dpad_emulation = None
            if ("dpad_emulation" in json_subpath and
                    json_subpath["dpad_emulation"]["position"] == component_name):
                matched_dpad_emulation = json_subpath["dpad_emulation"]

            monado_binding = json_subpath["monado_bindings"].get(component_name, None)

            steamvr_path = steamvr_subpath_name(identifier_json_path, json_subpath["type"])
            if "steamvr_path" in json_subpath:
                steamvr_path = json_subpath["steamvr_path"]

            c = Component(subaction_path,
                          identifier_json_path,
                          steamvr_path,
                          json_subpath["localized_name"],
                          json_subpath["type"],
                          component_name,
                          matched_dpad_emulation,
                          monado_binding,
                          json_subpath["components"])
            component_list.append(c)

        return component_list

    def __init__(self,
                 subaction_path,
                 identifier_json_path,
                 steamvr_path,
                 subpath_localized_name,
                 subpath_type,
                 component_name,
                 dpad_emulation,
                 monado_binding,
                 components_for_subpath):
        self.subaction_path = subaction_path
        self.identifier_json_path = identifier_json_path  # note: starts with a slash
        self.steamvr_path = steamvr_path
        self.subpath_localized_name = subpath_localized_name
        self.subpath_type = subpath_type
        self.component_name = component_name
        self.dpad_emulation = dpad_emulation
        self.monado_binding = monado_binding

        # click, touch etc. components under the subpath of this component.
        # Only needed for steamvr profile gen.
        self.components_for_subpath = components_for_subpath

    def get_full_openxr_paths(self):
        """A group of paths that derive from the same component.
        For example .../thumbstick, .../thumbstick/x, .../thumbstick/y
        """
        paths = []

        basepath = self.subaction_path + self.identifier_json_path

        if self.component_name == "position":
            paths.append(basepath + "/" + "x")
            paths.append(basepath + "/" + "y")
            paths.append(basepath)
        else:
            paths.append(basepath + "/" + self.component_name)
            paths.append(basepath)

        return paths

    def get_full_path(self):
        return self.subaction_path + self.identifier_json_path + '/' + self.component_name

    def is_input(self):
        # only haptics is output so far, everything else is input
        return self.component_name != "haptic"

    def has_dpad_emulation(self):
        return self.dpad_emulation is not None

    def is_output(self):
        return not self.is_input()


class Identifier:
    """A Identifier is a OpenXR identifier with a user path, such as button
    X, a trackpad, a pose such as aim. It can have one or more features, even
    tho outputs doesn't include a component/feature path a output identifier
    will have a haptic output feature.
    """

    @classmethod
    def parse_identifiers(indentifer_cls, json_profile):
        """Turn a profile's input paths into a list of Component objects."""

        json_subaction_paths = json_profile["subaction_paths"]
        json_subpaths = json_profile["subpaths"]

        identifier_list = []
        for subaction_path in json_subaction_paths:  # /user/hand/*
            for json_path in sorted(json_subpaths.keys()):  # /input/*, /output/*
                # json object associated with a subpath (type, localized_name, ...)
                json_subpath = json_subpaths[json_path]

                # Oculus Touch a,b/x,y components only exist on one controller
                if "side" in json_subpath and "/user/hand/" + json_subpath["side"] != subaction_path:
                    continue

                # Full path to the identifier
                identifier_path = subaction_path + json_path

                component_list = Component.parse_components(subaction_path,
                                                            json_path,
                                                            json_subpath)

                dpad = None
                if "dpad_emulation" in json_subpath:
                    dpad = DPad.parse_dpad(identifier_path,
                                           component_list,
                                           json_subpath["dpad_emulation"])

                i = Identifier(subaction_path,
                              identifier_path,
                              json_path,
                              component_list,
                              dpad)
                identifier_list.append(i)

        return identifier_list

    def __init__(self,
                 subaction_path,
                 identifier_path,
                 json_path,
                 component_list,
                 dpad):
        self.subaction_path = subaction_path
        self.identifier_path = identifier_path
        self.json_path = json_path
        self.components = component_list
        self.dpad = dpad
        return


class FeatureSet:
    """An AND of requirements (versions and/or extensions) under which a binding becomes available"""

    def __init__(self, required_extensions=None, required_version=None):
        self.required_extensions = frozenset(required_extensions if required_extensions is not None else [])
        self.required_version = required_version if required_version is not None else {"major": "0", "minor": "0"}

    def as_tuple(self):
        return (self.required_version["major"],
                self.required_version["minor"],
                sorted(self.required_extensions))

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

    def is_more_restrictive_than(self, other):
        if other.required_extensions.issuperset(self.required_extensions):
            # other requires extensions we don't
            return False
        if check_promoted(other.required_version) and not check_promoted(self.required_version):
            # other bounds version, we don't
            return False
        if check_promoted(other.required_version) and check_promoted(self.required_version):
            if other.required_version["major"] != self.required_version["major"]:
                # different major versions - not fully implemented, but this seems right
                return False
            if int(other.required_version["minor"]) > int(self.required_version["minor"]):
                # other has a higher lower bound on minor version than us
                return False

        return True

    def and_also(self, other):
        result = copy.deepcopy(self)
        result.required_extensions = self.required_extensions | other.required_extensions
        if not check_promoted(result.required_version):
            result.required_version = other.required_version
        elif check_promoted(other.required_version):
            if result.required_version["major"] != other.required_version["major"]:
                raise NotImplementedError("Major version mismatch not handled")
            if int(result.required_version["minor"]) < int(other.required_version["minor"]):
                result.required_version["minor"] = other.required_version["minor"]

        return result


class Availability:
    """An OR of FeatureSets, where any one of them being satisfied means a binding becomes available"""

    def __init__(self, feature_sets, optimize=True):
        if not optimize:
            self.feature_sets = set(feature_sets)
            return

        self.feature_sets = set()
        for feature_set in feature_sets:
            self.add_in_place(feature_set)

    def __str__(self) -> str:
        return f"{[str(fs) for fs in sorted(self.feature_sets, key=FeatureSet.as_tuple)]}"

    """Add an additional way for this availability to be satisfied"""
    def add_in_place(self, new_feature_set):
        for existing_feature in list(self.feature_sets):
            if existing_feature.is_more_restrictive_than(new_feature_set):
                self.feature_sets.remove(existing_feature)
            elif new_feature_set.is_more_restrictive_than(existing_feature):
                return
        self.feature_sets.add(new_feature_set)

    """Add an additional restriction to all feature sets"""
    def intersect_with_feature(self, feature_set):
        result = Availability(feature_sets=[])
        for existing_feature in self.feature_sets:
            result.add_in_place(existing_feature.and_also(feature_set))
        return result

    """Combine two availabilities into one that is satisfied if either is"""
    def union(self, other):
        result = copy.deepcopy(self)
        for feature_set in other.feature_sets:
            result.add_in_place(feature_set)

    """Combine two availabilities into one that is satisfied if both are.
       Note that this acts as a cartesian product followed by an at least n^2 op on that.
    """
    def intersection(self, other):
        features = []
        for feature_set in other.feature_sets:
            inter = self.intersect_with_feature(feature_set)
            features.extend(inter.feature_sets)

        return Availability(features)


class Profile:
    """An interactive bindings profile."""

    def __init__(self, profile_name, json_profile):
        """Construct an profile."""
        self.parent_profiles = set()
        self.name = profile_name
        self.localized_name = json_profile['title']
        self.profile_type = json_profile["type"]
        self.monado_device_enum = json_profile["monado_device"]
        self.validation_func_name = Profile.__strip_profile_prefix(
            profile_name).replace("/", "_")
        self.extension_name = json_profile.get("extension")
        self.extended_by = json_profile.get("extended_by")
        if self.extended_by is None:
            self.extended_by = []

        ov = json_profile.get("openxr_version")
        if ov is None:
            self.openxr_version_promoted = { "major" : "0", "minor" : "0" }
        else:
            promoted = ov.get("promoted")
            if promoted is None:
                self.openxr_version_promoted = { "major" : "0", "minor" : "0" }
            else:
                self.openxr_version_promoted = { "major" : promoted.get("major"), "minor" : promoted.get("minor") }

        self.is_virtual = profile_name.startswith("/virtual_profiles/")
        self.identifiers = Identifier.parse_identifiers(json_profile)

        self.steamvr_controller_type = None
        if "steamvr_controllertype" in json_profile:
            self.steamvr_controller_type = json_profile["steamvr_controllertype"]

        self.__update_component_list()
        collector = PathsByLengthCollector()
        for component in self.components:
            collector.add_paths(component.get_full_openxr_paths())
        self.subpaths_by_length = collector.to_dict_of_lists()

        collector = PathsByLengthCollector()
        for identifier in self.identifiers:
            if not identifier.dpad:
                continue
            collector.add_path(identifier.identifier_path)
        self.dpad_emulators_by_length = collector.to_dict_of_lists()

        collector = PathsByLengthCollector()
        for identifier in self.identifiers:
            if not identifier.dpad:
                continue
            path = identifier.identifier_path
            collector.add_paths(identifier.dpad.paths)
        self.dpad_paths_by_length = collector.to_dict_of_lists()

    @classmethod
    def __strip_profile_prefix(cls, profile_path):
        return profile_path.replace("/interaction_profiles/", "").replace("/virtual_profiles/", "")

    def is_parent_profile(self, child_profile):
        if child_profile == self:
            return False
        if child_profile.extended_by is None:
            return False
        parent_path = Profile.__strip_profile_prefix(self.name)
        return parent_path in child_profile.extended_by

    def merge_parent_profiles(self):
        self.identifiers = self.__get_merged_identifiers_helper({}).values()
        self.__update_component_list()

    def __get_merged_identifiers_helper(self, identifier_map):
        for ident in self.identifiers:
            if ident.identifier_path not in identifier_map:
                identifier_map[ident.identifier_path] = copy.deepcopy(ident)
                continue
            child_indent = identifier_map[ident.identifier_path]
            if child_indent.dpad is None:
                child_indent.dpad = ident.dpad
            child_comps = child_indent.components
            for parent_comp in ident.components:
                parent_path = parent_comp.get_full_path()
                child_exists = False
                for child_comp in child_comps:
                    if child_comp.get_full_path() == parent_path:
                        child_exists = True
                        break
                if not child_exists:
                    child_comps.append(parent_comp)

        parent_profiles = self.parent_profiles
        if parent_profiles is None or len(parent_profiles) == 0:
            return identifier_map
        else:
            for parent in parent_profiles:
                parent.__get_merged_identifiers_helper(identifier_map)
            return identifier_map

    def __update_component_list(self):
        self.components = []
        for identifier in self.identifiers:
            self.components += identifier.components
        self.components = sorted(self.components, key=attrgetter("steamvr_path"))

    def availability(self):
        result = Availability(feature_sets=[])
        has_requirements = False
        if check_promoted(self.openxr_version_promoted):
            result.add_in_place(FeatureSet(required_version=self.openxr_version_promoted))
            has_requirements = True
        if self.extension_name is not None:
            result.add_in_place(FeatureSet(required_extensions=[self.extension_name]))
            has_requirements = True
        if not has_requirements:
            result.add_in_place(FeatureSet())

        return result


oxr_verify_extension_status_struct_name = "oxr_verify_extension_status"


class Bindings:
    """A collection of interaction profiles used in bindings."""

    @classmethod
    def parse(cls, json_root):
        """Parse an entire bindings.json into a collection of Profile objects.
        """
        return cls(json_root)

    @classmethod
    def load_and_parse(cls, file):
        """Load a JSON file and parse it into Profile objects."""
        with open(file) as infile:
            json_root = json.loads(infile.read())
            return cls.parse(json_root)

    def __init__(self, json_root):
        """Construct a bindings from a dictionary of profiles."""
        self.profiles = [Profile(profile_name, json_root["profiles"][profile_name]) for
                         profile_name in sorted(json_root["profiles"].keys())]
        self.__set_parent_profile_refs()
        self.__mine_for_diamond_errors()

        self.virtual_profiles = [p for p in self.profiles if p.is_virtual]
        self.profiles = [p for p in self.profiles if not p.is_virtual]
        for profile in self.profiles:
            profile.merge_parent_profiles()

    def __set_parent_profile_refs(self):
        for profile1 in self.profiles:
            for profile2 in self.profiles:
                if profile1.is_parent_profile(profile2):
                    profile2.parent_profiles.add(profile1)

    def __mine_for_diamond_errors(self):
        for profile in self.profiles:
            parent_path_set = []
            if self.__has_diamonds(profile, parent_path_set):
                msg = f"Interaction Profile: {profile.name} in bindings.json has a diamond hierarchy, this is not supported."
                raise RuntimeError(msg)

    def __has_diamonds(self, profile, parent_path_set):
        if profile.name in parent_path_set:
            return True
        parent_path_set.append(profile.name)
        for parent in sorted(profile.parent_profiles, key=attrgetter("name")):
            if self.__has_diamonds(parent, parent_path_set):
                return True
        return False

header = '''// Copyright 2020-2022, Collabora, Ltd.
// SPDX-License-Identifier: BSL-1.0
/*!
 * @file
 * @brief  {brief}.
 * @author Jakob Bornecrantz <jakob@collabora.com>
 * @author Christoph Haag <christoph.haag@collabora.com>
 * @author Korcan Hussein <korcan.hussein@collabora.com>
 * @ingroup {group}
 */
'''

func_start = '''
bool
{name}(const struct oxr_extension_status *exts, XrVersion openxr_version, const char *str, size_t length)
{{
'''


def write_verify_func_begin(f, name):
    f.write(func_start.format(
        name=name, ext_status_struct_name=oxr_verify_extension_status_struct_name))


def write_verify_func_end(f):
    f.write("\treturn false;\n}\n")


if_strcmp = '''if (strcmp(str, "{check}") == 0) {{
{exttab}\t\t\treturn true;
{exttab}\t\t}} else '''

def check_promoted(openxr_version_promoted):
    # If required version is 0.0, we can skip checking that the instance uses a more recent version
    return openxr_version_promoted is not None and not (openxr_version_promoted["major"] == '0' and openxr_version_promoted["minor"] == '0')

def write_verify_switch_body(f, dict_of_lists, profile, profile_name, tab_char):
    """Generate function to check if a string is in a set of strings.
    Input is a file to write the code into, a dict where keys are length and
    the values are lists of strings of that length. And a suffix if any."""
    f.write(f"{tab_char}\tswitch (length) {{\n")
    for length in sorted(dict_of_lists.keys()):
        f.write(f"{tab_char}\tcase {str(length)}:\n\t\t{tab_char}")
        for path in sorted(dict_of_lists[length]):
            f.write(if_strcmp.format(exttab=tab_char, check=path))
        f.write(f"{{\n{tab_char}\t\t\tbreak;\n{tab_char}\t\t}}\n")
    f.write(f"{tab_char}\tdefault: break;\n{tab_char}\t}}\n")

def write_verify_func_switch(f, dict_of_lists, profile, profile_name, availability):
    """Generate function to check if a string is in a set of strings.
    Input is a file to write the code into, a dict where keys are length and
    the values are lists of strings of that length. And a suffix if any."""
    if len(dict_of_lists) == 0:
        return

    f.write(f"\t// generated from: {profile_name}\n")

    # Example: pico neo 3 can be enabled by either enabling XR_BD_controller_interaction ext or using OpenXR 1.1+.
    # Disabling OXR_HAVE_BD_controller_interaction should NOT remove pico neo from OpenXR 1.1+ (it makes "exts->BD_controller_interaction" invalid C code).
    # Therefore separate code blocks for ext and version checks generated to avoid ifdef hell.
    feature_sets = sorted(availability.feature_sets, key=FeatureSet.as_tuple)
    for feature_set in feature_sets:
        requires_version = check_promoted(feature_set.required_version)
        requires_extensions = bool(feature_set.required_extensions)

        tab_char = ''
        closing = []

        if requires_version:
            tab_char += '\t'
            f.write(f'{tab_char}if (openxr_version >= XR_MAKE_VERSION({feature_set.required_version["major"]}, {feature_set.required_version["minor"]}, 0)) {{\n')
            closing.append(f'{tab_char}}}\n')

        if requires_extensions:
            tab_char += '\t'
            exts = sorted(feature_set.required_extensions)
            ext_defines = ' && '.join(f'defined(OXR_HAVE_{ext})' for ext in exts)
            f.write(f'#if {ext_defines}\n')
            f.write(f'{tab_char}if ('+' && '.join(f'exts->{ext}' for ext in exts)+') {\n')
            closing.append(f'{tab_char}}}\n#endif // {ext_defines}\n')

        write_verify_switch_body(f, dict_of_lists, profile, profile_name, tab_char)

        for closer in reversed(closing):
            f.write(closer)

def write_verify_func_body(f, profile, dict_name, availability):
    if profile is None or dict_name is None or len(dict_name) == 0:
        return
    write_verify_func_switch(f, getattr(
        profile, dict_name), profile, profile.name, availability)
    if profile.parent_profiles is None:
        return
    for pp in sorted(profile.parent_profiles, key=attrgetter("name")):
        write_verify_func_body(f, pp, dict_name, availability.intersection(pp.availability()))


def write_verify_func(f, profile, dict_name, suffix):
    write_verify_func_begin(
        f, f"oxr_verify_{profile.validation_func_name}{suffix}")
    write_verify_func_body(f, profile, dict_name, profile.availability())
    write_verify_func_end(f)


def generate_verify_functions(f, profile):
    write_verify_func(f, profile, "subpaths_by_length", "_subpath")
    write_verify_func(f, profile, "dpad_paths_by_length", "_dpad_path")
    write_verify_func(f, profile, "dpad_emulators_by_length", "_dpad_emulator")

    f.write(f'''
void
oxr_verify_{profile.validation_func_name}_ext(const struct oxr_extension_status *extensions, XrVersion openxr_version, bool *out_supported, bool *out_enabled)
{{
''')
    is_promoted = check_promoted(profile.openxr_version_promoted)
    if is_promoted:
        f.write(f'\tif (openxr_version >= XR_MAKE_VERSION({profile.openxr_version_promoted["major"]}, {profile.openxr_version_promoted["minor"]}, 0)) {{\n')
        f.write(f'\t\t*out_supported = true;\n')
        f.write(f'\t\t*out_enabled = true;\n')
        f.write(f'\t\treturn;\n')
        f.write(f'\t}}')


    if profile.extension_name is not None:
        f.write(f'''
#ifdef OXR_HAVE_{profile.extension_name}
\t*out_supported = true;
\t*out_enabled = extensions->{profile.extension_name};
#else
\t*out_supported = false;
\t*out_enabled = false;
#endif // OXR_HAVE_{profile.extension_name}
''')

    else:
        f.write(f'''
\t*out_supported = true;
\t*out_enabled = true;
''')

    f.write(f'''}}
''')


def generate_bindings_c(file, b):
    """Generate the file to verify subpaths on a interaction profile."""
    f = open(file, "w")
    f.write(header.format(brief='Generated bindings data', group='oxr_main'))
    f.write('''
#include "b_generated_bindings.h"
#include <string.h>
#include <oxr_objects.h>

// clang-format off
''')

    for profile in b.profiles:
        generate_verify_functions(f, profile)

    f.write(
        f'\n\nstruct profile_template profile_templates[{len(b.profiles)}] = {{ // array of profile_template\n')
    for profile in b.profiles:
        hw_name = str(profile.name.split("/")[-1])
        vendor_name = str(profile.name.split("/")[-2])
        fname = vendor_name + "_" + hw_name + "_profile.json"
        controller_type = "monado_" + vendor_name + "_" + hw_name

        binding_count = len(profile.components)
        f.write(f'\t{{ // profile_template\n')
        f.write(f'\t\t.name = {profile.monado_device_enum},\n')
        f.write(f'\t\t.path = "{profile.name}",\n')
        f.write(f'\t\t.localized_name = "{profile.localized_name}",\n')
        f.write(f'\t\t.steamvr_input_profile_path = "{fname}",\n')
        f.write(f'\t\t.steamvr_controller_type = "{controller_type}",\n')
        f.write(f'\t\t.binding_count = {binding_count},\n')
        f.write(
            f'\t\t.bindings = (struct binding_template[]){{ // array of binding_template\n')

        component: Component
        for idx, component in enumerate(profile.components):

            # @todo Doesn't handle pose yet.
            steamvr_path = component.steamvr_path
            if component.component_name in ["click", "touch", "force", "value", "proximity"]:
                steamvr_path += "/" + component.component_name

            f.write(f'\t\t\t{{ // binding_template {idx}\n')
            f.write(f'\t\t\t\t.subaction_path = "{component.subaction_path}",\n')
            f.write(f'\t\t\t\t.steamvr_path = "{steamvr_path}",\n')
            f.write(
                f'\t\t\t\t.localized_name = "{component.subpath_localized_name}",\n')

            f.write('\t\t\t\t.paths = { // array of paths\n')
            for path in component.get_full_openxr_paths():
                f.write(f'\t\t\t\t\t"{path}",\n')
            f.write('\t\t\t\t\tNULL\n')
            f.write('\t\t\t\t}, // /array of paths\n')

            # print("component", component.__dict__)

            component_str = component.component_name

            # controllers can have input that we don't have bindings for
            if component.monado_binding:
                monado_binding = component.monado_binding

                if component.is_input() and monado_binding is not None:
                    f.write(f'\t\t\t\t.input = {monado_binding},\n')
                else:
                    f.write(f'\t\t\t\t.input = 0,\n')

                if component.has_dpad_emulation() and "activate" in component.dpad_emulation:
                    activate_component = find_component_in_list_by_name(
                        component.dpad_emulation["activate"], profile.components,
                        subaction_path=component.subaction_path,
                        identifier_json_path=component.identifier_json_path)
                    f.write(
                        f'\t\t\t\t.dpad_activate = {activate_component.monado_binding},\n')
                else:
                    f.write(f'\t\t\t\t.dpad_activate = 0,\n')

                if component.is_output() and monado_binding is not None:
                    f.write(f'\t\t\t\t.output = {monado_binding},\n')
                else:
                    f.write(f'\t\t\t\t.output = 0,\n')
            f.write(f'\t\t\t}}, // /binding_template {idx}\n')

        f.write('\t\t}, // /array of binding_template\n')

        dpads = []
        for idx, identifier in enumerate(profile.identifiers):
            if identifier.dpad:
                dpads.append(identifier)

#        for identifier in dpads:
#            print(identifier.path, identifier.dpad_position_component)

        dpad_count = len(dpads)
        f.write(f'\t\t.dpad_count = {dpad_count},\n')
        if len(dpads) == 0:
            f.write(f'\t\t.dpads = NULL,\n')
        else:
            f.write(
                f'\t\t.dpads = (struct dpad_emulation[]){{ // array of dpad_emulation\n')
            for idx, identifier in enumerate(dpads):
                f.write('\t\t\t{\n')
                f.write(f'\t\t\t\t.subaction_path = "{identifier.subaction_path}",\n')
                f.write('\t\t\t\t.paths = {\n')
                for path in identifier.dpad.paths:
                    f.write(f'\t\t\t\t\t"{path}",\n')
                f.write('\t\t\t\t},\n')
                f.write(f'\t\t\t\t.position = {identifier.dpad.position_component.monado_binding},\n')
                if identifier.dpad.activate_component:
                    f.write(f'\t\t\t\t.activate = {identifier.dpad.activate_component.monado_binding},\n')
                else:
                    f.write(f'\t\t\t\t.activate = 0')

                f.write('\t\t\t},\n')
            f.write('\t\t}, // /array of dpad_emulation\n')

        f.write(f'\t\t.openxr_version.promoted.major = {profile.openxr_version_promoted["major"]},\n')
        f.write(f'\t\t.openxr_version.promoted.minor = {profile.openxr_version_promoted["minor"]},\n')

        fn_prefixes = ["subpath", "dpad_path", "dpad_emulator"]
        for prefix in fn_prefixes:
            f.write(f'\t\t.{prefix}_fn = oxr_verify_{profile.validation_func_name}_{prefix},\n')
        f.write(f'\t\t.ext_verify_fn = oxr_verify_{profile.validation_func_name}_ext,\n')
        if profile.extension_name is None:
            f.write(f'\t\t.extension_name = NULL,\n')
        else:
            f.write(f'\t\t.extension_name = "{profile.extension_name}",\n')
        f.write('\t}, // /profile_template\n')

    f.write('}; // /array of profile_template\n\n')

    inputs = set()
    outputs = set()
    for profile in b.profiles:
        component: Component
        for idx, component in enumerate(profile.components):

            if not component.monado_binding:
                continue

            if component.subpath_type == "vibration":
                outputs.add(component.monado_binding)
            else:
                inputs.add(component.monado_binding)

    # special cased bindings that are never directly used in the input profiles
    inputs.add("XRT_INPUT_GENERIC_HEAD_POSE")
    inputs.add("XRT_INPUT_GENERIC_HEAD_DETECT")
    inputs.add("XRT_INPUT_GENERIC_HAND_TRACKING_LEFT")
    inputs.add("XRT_INPUT_GENERIC_HAND_TRACKING_RIGHT")
    inputs.add("XRT_INPUT_GENERIC_TRACKER_POSE")

    f.write('const char *\n')
    f.write('xrt_input_name_string(enum xrt_input_name input)\n')
    f.write('{\n')
    f.write('\tswitch(input)\n')
    f.write('\t{\n')
    for input in sorted(inputs):
        f.write(f'\tcase {input}: return "{input}";\n')
    f.write(f'\tdefault: return "UNKNOWN";\n')
    f.write('\t}\n')
    f.write('}\n')

    f.write('enum xrt_input_name\n')
    f.write('xrt_input_name_enum(const char *input)\n')
    f.write('{\n')
    for input in sorted(inputs):
        f.write(f'\tif(strcmp("{input}", input) == 0) return {input};\n')
    f.write(f'\treturn XRT_INPUT_GENERIC_TRACKER_POSE;\n')
    f.write('}\n')

    f.write('const char *\n')
    f.write('xrt_output_name_string(enum xrt_output_name output)\n')
    f.write('{\n')
    f.write('\tswitch(output)\n')
    f.write('\t{\n')
    for output in sorted(outputs):
        f.write(f'\tcase {output}: return "{output}";\n')
    f.write(f'\tdefault: return "UNKNOWN";\n')
    f.write('\t}\n')
    f.write('}\n')

    f.write('enum xrt_output_name\n')
    f.write('xrt_output_name_enum(const char *output)\n')
    f.write('{\n')
    for output in sorted(outputs):
        f.write(f'\tif(strcmp("{output}", output) == 0) return {output};\n')
    f.write(f'\treturn XRT_OUTPUT_NAME_SIMPLE_VIBRATION;\n')
    f.write('}\n')

    f.write(f'''

static const struct oxr_bindings_path_cache internal_path_cache = {{
\t.path_cache = {{''')
    for profile_index, _ in enumerate(b.profiles):
        f.write(f'''
\t\t{{
\t\t\t.path_cache = &profile_templates[{profile_index}].path_cache,
\t\t\t.path_cache_name = &profile_templates[{profile_index}].path,
\t\t}},\n''')
        profile_index += 1
    f.write(f'''\t}}
}};

void oxr_get_interaction_profile_path_cache(const struct oxr_bindings_path_cache **out_path_cache)
{{
    *out_path_cache = &internal_path_cache;
}}
''')
    f.write("\n// clang-format on\n")

    f.close()


def generate_bindings_h(file, b):
    """Generate header for the verify subpaths functions."""
    f = open(file, "w")
    f.write(header.format(brief='Generated bindings data header',
                          group='oxr_api'))
    f.write(f'''
#pragma once

#include <stddef.h>

#include "xrt/xrt_defines.h"

typedef uint64_t XrPath; // OpenXR typedef
typedef uint64_t XrVersion; // OpenXR typedef

struct oxr_extension_status;

#define OXR_BINDINGS_PROFILE_TEMPLATE_COUNT {len(b.profiles)}

struct oxr_bindings_path_cache_element {{
    //! Pointer to XrPath
    XrPath *path_cache;
    //! Pointer to char*
    const char **path_cache_name;
}};

struct oxr_bindings_path_cache {{
    // wrapped in a struct solely to reduce the C pointer soup
    struct oxr_bindings_path_cache_element path_cache[OXR_BINDINGS_PROFILE_TEMPLATE_COUNT];
}};

void oxr_get_interaction_profile_path_cache(const struct oxr_bindings_path_cache **out_path_cache);

// clang-format off
''')

    fn_prefixes = ["_subpath", "_dpad_path", "_dpad_emulator"]
    for profile in b.profiles:
        for fn_suffix in fn_prefixes:
            f.write(
                f"\nbool\noxr_verify_{profile.validation_func_name}{fn_suffix}(const struct oxr_extension_status *extensions, XrVersion openxr_major_minor, const char *str, size_t length);\n")
        f.write(f'''\nvoid\noxr_verify_{profile.validation_func_name}_ext(const struct oxr_extension_status *extensions, XrVersion openxr_version, bool *out_supported, bool *out_enabled);\n''')

    f.write(f'''
#define PATHS_PER_BINDING_TEMPLATE 16

enum oxr_dpad_binding_point
{{
\tOXR_DPAD_BINDING_POINT_NONE,
\tOXR_DPAD_BINDING_POINT_UP,
\tOXR_DPAD_BINDING_POINT_DOWN,
\tOXR_DPAD_BINDING_POINT_LEFT,
\tOXR_DPAD_BINDING_POINT_RIGHT,
}};

struct dpad_emulation
{{
\tconst char *subaction_path;
\tconst char *paths[PATHS_PER_BINDING_TEMPLATE];
\tenum xrt_input_name position;
\tenum xrt_input_name activate; // Can be zero
}};

struct binding_template
{{
\tconst char *subaction_path;
\tconst char *steamvr_path;
\tconst char *localized_name;
\tconst char *paths[PATHS_PER_BINDING_TEMPLATE];
\tenum xrt_input_name input;
\tenum xrt_input_name dpad_activate;
\tenum xrt_output_name output;
}};

typedef bool (*path_verify_fn_t)(const struct oxr_extension_status *extensions, XrVersion openxr_version, const char *, size_t);
typedef void (*ext_verify_fn_t)(const struct oxr_extension_status *extensions, XrVersion openxr_version, bool *out_supported, bool *out_enabled);

struct profile_template
{{
\tenum xrt_device_name name;
\tconst char *path;
\tconst char *localized_name;
\tconst char *steamvr_input_profile_path;
\tconst char *steamvr_controller_type;
\tstruct binding_template *bindings;
\tsize_t binding_count;
\tstruct dpad_emulation *dpads;
\tsize_t dpad_count;
\tstruct {{
\t\tstruct {{
\t\t\tuint32_t major;
\t\t\tuint32_t minor;
\t\t}} promoted;
\t}} openxr_version;
\t// Only valid after path cache entries are initialized via oxr_get_interaction_profile_path_cache.
\tXrPath path_cache;

\tpath_verify_fn_t subpath_fn;
\tpath_verify_fn_t dpad_path_fn;
\tpath_verify_fn_t dpad_emulator_fn;
\text_verify_fn_t ext_verify_fn;
\tconst char *extension_name;
}};

extern struct profile_template profile_templates[OXR_BINDINGS_PROFILE_TEMPLATE_COUNT];

''')

    f.write('const char *\n')
    f.write('xrt_input_name_string(enum xrt_input_name input);\n\n')

    f.write('enum xrt_input_name\n')
    f.write('xrt_input_name_enum(const char *input);\n\n')

    f.write('const char *\n')
    f.write('xrt_output_name_string(enum xrt_output_name output);\n\n')

    f.write('enum xrt_output_name\n')
    f.write('xrt_output_name_enum(const char *output);\n\n')

    f.write("\n// clang-format on\n")
    f.close()


def main():
    """Handle command line and generate a file."""
    parser = argparse.ArgumentParser(description='Bindings generator.')
    parser.add_argument(
        'bindings', help='Bindings file to use')
    parser.add_argument(
        'output', type=str, nargs='+',
        help='Output file, uses the name to choose output type')
    args = parser.parse_args()

    bindings = Bindings.load_and_parse(args.bindings)

    for output in args.output:
        if output.endswith("generated_bindings.c"):
            generate_bindings_c(output, bindings)
        if output.endswith("generated_bindings.h"):
            generate_bindings_h(output, bindings)


if __name__ == "__main__":
    main()
