"""Metadata mutation."""

from collections import namedtuple
from typing import Optional

import common_helpers

RESERVED_WORDS = [
    "abstract",
    "as",
    "base",
    "bool",
    "break",
    "byte",
    "case",
    "catch",
    "char",
    "checked",
    "class",
    "const",
    "continue",
    "decimal",
    "default",
    "delegate",
    "do",
    "double",
    "else",
    "enum",
    "event",
    "explicit",
    "extern",
    "false",
    "finally",
    "fixed",
    "float",
    "for",
    "foreach",
    "goto",
    "if",
    "implicit",
    "in",
    "int",
    "interface",
    "internal",
    "is",
    "lock",
    "long",
    "namespace",
    "new",
    "null",
    "object",
    "operator",
    "out",
    "override",
    "params",
    "private",
    "protected",
    "public",
    "readonly",
    "ref",
    "return",
    "sbyte",
    "sealed",
    "short",
    "sizeof",
    "stackalloc",
    "static",
    "string",
    "struct",
    "switch",
    "this",
    "throw",
    "true",
    "try",
    "typeof",
    "uint",
    "ulong",
    "unchecked",
    "unsafe",
    "ushort",
    "using",
    "virtual",
    "void",
    "volatile",
    "while",
]

# These are all of the coerced narrow numerics used in RFmx and DAQmx.
# This will not account for other aliases of narrow numerics.
# Note that params can also be marked coerced instead of updating this list.
# uint8 is not included because it can be represented as a byte.
KNOWN_COERCED_NARROW_NUMERIC_TYPES = ["int16", "uInt16", "int8"]


def sanitize_names(parameters):
    """Sanitize name fields on a list of parameter objects.

    Also, populate the cppname field with the sanitized value.
    """
    for parameter in parameters:
        if "callback_params" in parameter:
            sanitize_names(parameter["callback_params"])
        parameter["cppName"] = parameter["name"]
        if parameter["cppName"] in RESERVED_WORDS:
            parameter["cppName"] += "Parameter"


def set_var_args_types(parameters, config):
    """Set information about varargs parameters in the metadata."""
    for parameter in parameters:
        if common_helpers.is_repeated_varargs_parameter(parameter):
            parameter["type"] = "..."


def mark_size_params(parameters):
    """Mark the size parameters in the metadata."""
    for param in parameters:
        mechanism = common_helpers.get_size_mechanism(param)
        if mechanism in {
            "len",
            "ivi-dance",
            "passed-in",
            "passed-in-by-ptr",
            "ivi-dance-with-a-twist",
        }:
            size_param = _get_size_param(param, parameters)
            size_param["is_size_param"] = True
            if mechanism == "passed-in-by-ptr":
                size_param["pointer"] = True


def mark_coerced_narrow_numeric_parameters(parameters: dict) -> None:
    """Mark parameters with narrow numeric types as coerced."""
    for param in parameters:
        param_type = common_helpers.get_underlying_type(param)
        if param_type in KNOWN_COERCED_NARROW_NUMERIC_TYPES:
            param["coerced"] = True


def mark_non_proto_params(parameters):
    """Mark the parameters that shouldn't be included in the proto request message.

    Their values should be derived from other sources in the service handlers.
    """
    for param in parameters:
        mechanism = common_helpers.get_size_mechanism(param)
        if mechanism in {"len", "ivi-dance", "ivi-dance-with-a-twist"}:
            size_param = _get_size_param(param, parameters)
            if size_param["direction"] == "in":
                # Output size_params can still be included in the proto
                # as information.
                size_param["include_in_proto"] = False
            if mechanism == "len":
                if "determine_size_from" not in size_param:
                    size_param["determine_size_from"] = []
                size_param["determine_size_from"].append(param["name"])
                is_optional = common_helpers.is_optional(param)
                if is_optional != size_param.get("linked_params_are_optional", is_optional):
                    raise Exception(
                        "Code generator does not support linked params that are a mix of optional and required",
                        size_param,
                    )
                size_param["linked_params_are_optional"] = is_optional


def _get_size_param(param, parameters):
    named_params = {p["name"]: p for p in parameters}
    return named_params.get(param["size"]["value"], None)


def mark_mapped_enum_params(parameters, enums):
    """Mark enum paramaters as mapped-enum if the enum has mappings."""
    for param in (p for p in parameters if "enum" in p):
        enum_name = param["enum"]
        enum = enums[enum_name]
        if "generate-mappings" in enum:
            del param["enum"]
            param["mapped-enum"] = enum_name


def populate_grpc_types(parameters, config):
    """Set the grpc_type of each parameter that doesn't already have it set."""
    for parameter in parameters:
        if "callback_params" in parameter:
            populate_grpc_types(parameter["callback_params"], config)
        if "grpc_type" in parameter:
            continue
        parameter["grpc_type"] = common_helpers.get_grpc_type(parameter["type"], config)


def _get_short_enum_type_name(typename: str) -> str:
    if typename in ["char[]", "const char[]", "char"]:
        return "String"
    stripped_name = common_helpers.strip_prefix(typename, "Vi")
    return common_helpers.ensure_pascal_case(stripped_name)


def _remove_leading_group_name(enum_value_name, group_name):
    return common_helpers.strip_prefix(enum_value_name, f"{group_name.upper()}_")


def _add_leading_enum_name(enum_value_name, enum_name, enum):
    enum_value_prefix = enum.get(
        "enum-value-prefix", common_helpers.pascal_to_snake(enum_name).upper()
    )
    return f"{enum_value_prefix}_{enum_value_name}"


def _add_attribute_values_enums(enums, attribute_enums_by_type, group_name):
    """Add new enums to enums metadata, for use as the value parameter of SetAttribute APIs."""
    for type_name in attribute_enums_by_type:
        unmapped_values = {}
        mapped_values = {}
        for enum_name in sorted(attribute_enums_by_type[type_name]):
            enum = enums[enum_name]
            is_mapped_enum = enum.get("generate-mappings", False)
            for value in enum["values"]:
                # Remove the leading group name (if any) because it will be redundant in the
                # aggregate enum.
                value_name = _remove_leading_group_name(value["name"], group_name)
                # Add a leading enum to differentiate sub-enums within the aggregate values enum.
                value_name = _add_leading_enum_name(value_name, enum_name, enum)
                if is_mapped_enum:
                    mapped_values[value_name] = value["value"]
                else:
                    unmapped_values[value_name] = value["value"]

        shortened_type_name = _get_short_enum_type_name(type_name)
        enum_value_prefix = (f"{group_name}_{shortened_type_name}").upper()
        unmapped_enum_name = _get_attribute_values_enum_name(group_name, type_name)
        mapped_enum_name = _get_attribute_values_enum_name(group_name, type_name, is_mapped=True)
        _add_enum(unmapped_enum_name, unmapped_values, enums, enum_value_prefix)
        _add_enum(mapped_enum_name, mapped_values, enums, enum_value_prefix, is_mapped=True)


AttributeReferencingParameter = namedtuple(
    "AttributeReferencingParameter", ["attribute_group", "parameter"]
)


class AttributeAccessorExpander:
    """Wraps an _attribute_type_map of: group -> type -> attributes.

    Also implements the metadata_mutations to _add_attribute_values_enums,
    _expand_attribute_function_value_param, and patch_attribute_enum_type.
    """

    def __init__(self, metadata):  # noqa: D107
        self._config = metadata["config"]
        self._enums = metadata["enums"]
        self._attribute_type_map = {}

        for group in common_helpers.get_attribute_groups(metadata):
            attribute_enums_by_type = common_helpers.get_attribute_enums_by_type(group.attributes)
            _add_attribute_values_enums(self._enums, attribute_enums_by_type, group.name)
            self._attribute_type_map[group.name] = attribute_enums_by_type

    def _get_attribute_reference_parameter(
        self, parameters
    ) -> Optional[AttributeReferencingParameter]:
        """Get the parameter that references an attribute, if there is one."""

        def try_resolve_attribute_reference(parameter) -> Optional[AttributeReferencingParameter]:
            param_type = parameter["grpc_type"]
            # All attribute parameters must have a grpc_type of {group_name}Attributes.
            # In MI, this is added during metadata mutation of ViAttr types.
            attr_suffix = common_helpers.get_attribute_enum_suffix(self._config)
            potential_attribute_name = common_helpers.strip_suffix(param_type, attr_suffix)
            if potential_attribute_name in self._attribute_type_map:
                return AttributeReferencingParameter(potential_attribute_name, parameter)
            return None

        # Assumption: there are 0 or 1 parameters that reference an attribute per function.
        attribute_resolve_results = (try_resolve_attribute_reference(p) for p in parameters)
        valid_references = (ref for ref in attribute_resolve_results if ref)
        return next(valid_references, None)

    def patch_attribute_enum_type(self, function_name, func):
        """Update the attribute parameter to point to the proper enum name.

        Only applies for drivers that splits attribute enums by type.

        For example, DAQmx has a Get/Set/ResetBufferAttributeUInt32 functions. The parameter that
        selects _which_ attribute needs to use the proper attribute enum. The input says that the
        grpc_type is BufferAttribute, but the generated code splits the enums by type, with a
        special group for Reset functions. So in this example, the Get and Set functions' attribute
        enum would be BufferUInt32Attribute, and the Reset function's attribute enum would be
        BufferResetAttribute.
        """
        if not common_helpers.get_split_attributes_by_type(self._config):
            return
        parameters = func["parameters"]
        attribute_reference = self._get_attribute_reference_parameter(parameters)
        if attribute_reference:
            group = attribute_reference.attribute_group
            attribute_param = attribute_reference.parameter
            if function_name.startswith("Reset"):
                sub_group = "Reset"
            else:
                value_param = _get_attribute_function_value_param(func)
                sub_group = common_helpers.get_grpc_type_name_for_identifier(
                    value_param["type"], self._config
                )
            if common_helpers.supports_raw_attributes(self._config):
                attribute_param["enum"] = common_helpers.get_attribute_enum_name(
                    group, sub_group, self._config
                )
                attribute_param["grpc_type"] = "int32"
                attribute_param["raw_attribute"] = True
            else:
                attribute_param["grpc_type"] = common_helpers.get_attribute_enum_name(
                    group, sub_group, self._config
                )

    def expand_attribute_value_params(self, func):
        """Update the attrVal parameter to use the proper enum, if available."""
        parameters = func["parameters"]
        attribute_reference = self._get_attribute_reference_parameter(parameters)
        if attribute_reference:
            _expand_attribute_function_value_param(
                func,
                self._enums,
                self._attribute_type_map[attribute_reference.attribute_group],
                attribute_reference.attribute_group,
            )


def _expand_attribute_function_value_param(
    function, enums, attribute_enums_by_type, service_class_prefix
):
    """For SetAttribute and CheckAttribute, update function metadata to mark value param as enum."""
    value_param = _get_attribute_function_value_param(function)
    if not value_param:
        return
    if value_param["direction"] == "out":
        if common_helpers.is_static_castable_enum_type(value_param):
            value_param["use_checked_enum_conversion"] = True
        else:
            return

    if value_param["type"] == "ViConstString":
        param_type = "ViString"
    else:
        param_type = common_helpers.get_underlying_type(value_param)
    if param_type in attribute_enums_by_type:
        enum_name = _get_attribute_values_enum_name(service_class_prefix, param_type)
        mapped_enum_name = _get_attribute_values_enum_name(
            service_class_prefix, param_type, is_mapped=True
        )
        enum_exists = enum_name in enums
        mapped_enum_exists = mapped_enum_name in enums
        if enum_exists:
            value_param["enum"] = enum_name
        if mapped_enum_exists:
            value_param["mapped-enum"] = mapped_enum_name
        if not enum_exists and not mapped_enum_exists:
            # Ideally there must be an enum associated with this parameter for SetAttribute* API.
            # Since the enum is empty, users will be passing in raw values. Make it clear via
            # naming.
            value_param["name"] += "_raw"


def _get_attribute_function_value_param(function):
    return next(
        (
            param
            for param in function["parameters"]
            if param["name"] in {"value", "attributeValue", "attrVal"}
        ),
        None,
    )


def _get_attribute_values_enum_name(group_name, type, is_mapped=False):
    enum_name_suffix = "Mapped" if is_mapped else ""
    shortened_type_name = _get_short_enum_type_name(type)
    return group_name + shortened_type_name + "AttributeValues" + enum_name_suffix


def _add_enum(enum_name, enum_values, enums, enum_value_prefix, is_mapped=False):
    if not enum_values:
        return
    values = [{"name": name, "value": enum_values[name]} for name in enum_values]
    new_enum = {
        "enum-value-prefix": enum_value_prefix,
        "generate-mappings": is_mapped,
        "values": values,
    }
    enums.update({enum_name: new_enum})


def move_zero_enums_to_front(enums: dict) -> None:
    """Put the enum value with value 0 _first_ if there is one, or else add UNSPECIFIED enum value.

    protobuf requires that the first enum value be zero. For enums missing a zero value,
    we will add an UNSPECIFIED enum value to the front (this is the best practice). But if
    there already is a zero enum, make sure that pre-existing zero value is at the front.
    """
    for enum in enums.values():
        values = enum["values"]
        try:
            i, _ = next((i, v) for i, v in enumerate(values) if v["value"] == 0)
            values.insert(0, values.pop(i))
        except StopIteration:
            pass

"""Metadata mutation."""

from collections import namedtuple
from typing import Optional

import common_helpers

RESERVED_WORDS = [
    "abstract",
    "as",
    "base",
    "bool",
    "break",
    "byte",
    "case",
    "catch",
    "char",
    "checked",
    "class",
    "const",
    "continue",
    "decimal",
    "default",
    "delegate",
    "do",
    "double",
    "else",
    "enum",
    "event",
    "explicit",
    "extern",
    "false",
    "finally",
    "fixed",
    "float",
    "for",
    "foreach",
    "goto",
    "if",
    "implicit",
    "in",
    "int",
    "interface",
    "internal",
    "is",
    "lock",
    "long",
    "namespace",
    "new",
    "null",
    "object",
    "operator",
    "out",
    "override",
    "params",
    "private",
    "protected",
    "public",
    "readonly",
    "ref",
    "return",
    "sbyte",
    "sealed",
    "short",
    "sizeof",
    "stackalloc",
    "static",
    "string",
    "struct",
    "switch",
    "this",
    "throw",
    "true",
    "try",
    "typeof",
    "uint",
    "ulong",
    "unchecked",
    "unsafe",
    "ushort",
    "using",
    "virtual",
    "void",
    "volatile",
    "while",
]

# These are all of the coerced narrow numerics used in RFmx and DAQmx.
# This will not account for other aliases of narrow numerics.
# Note that params can also be marked coerced instead of updating this list.
# uint8 is not included because it can be represented as a byte.
KNOWN_COERCED_NARROW_NUMERIC_TYPES = ["int16", "uInt16", "int8"]


def sanitize_names(parameters):
    """Sanitize name fields on a list of parameter objects.

    Also, populate the cppname field with the sanitized value.
    """
    for parameter in parameters:
        if "callback_params" in parameter:
            sanitize_names(parameter["callback_params"])
        parameter["cppName"] = parameter["name"]
        if parameter["cppName"] in RESERVED_WORDS:
            parameter["cppName"] += "Parameter"


def set_var_args_types(parameters, config):
    """Set information about varargs parameters in the metadata."""
    for parameter in parameters:
        if common_helpers.is_repeated_varargs_parameter(parameter):
            parameter["type"] = "..."


def mark_size_params(parameters):
    """Mark the size parameters in the metadata."""
    for param in parameters:
        mechanism = common_helpers.get_size_mechanism(param)
        if mechanism in {
            "len",
            "ivi-dance",
            "passed-in",
            "passed-in-by-ptr",
            "ivi-dance-with-a-twist",
        }:
            size_param = _get_size_param(param, parameters)
            size_param["is_size_param"] = True
            if mechanism == "passed-in-by-ptr":
                size_param["pointer"] = True


def mark_coerced_narrow_numeric_parameters(parameters: dict) -> None:
    """Mark parameters with narrow numeric types as coerced."""
    for param in parameters:
        param_type = common_helpers.get_underlying_type(param)
        if param_type in KNOWN_COERCED_NARROW_NUMERIC_TYPES:
            param["coerced"] = True


def mark_non_proto_params(parameters):
    """Mark the parameters that shouldn't be included in the proto request message.

    Their values should be derived from other sources in the service handlers.
    """
    for param in parameters:
        mechanism = common_helpers.get_size_mechanism(param)
        if mechanism in {"len", "ivi-dance", "ivi-dance-with-a-twist"}:
            size_param = _get_size_param(param, parameters)
            if size_param["direction"] == "in":
                # Output size_params can still be included in the proto
                # as information.
                size_param["include_in_proto"] = False
            if mechanism == "len":
                if "determine_size_from" not in size_param:
                    size_param["determine_size_from"] = []
                size_param["determine_size_from"].append(param["name"])
                is_optional = common_helpers.is_optional(param)
                if is_optional != size_param.get("linked_params_are_optional", is_optional):
                    raise Exception(
                        "Code generator does not support linked params that are a mix of optional and required",
                        size_param,
                    )
                size_param["linked_params_are_optional"] = is_optional


def _get_size_param(param, parameters):
    named_params = {p["name"]: p for p in parameters}
    return named_params.get(param["size"]["value"], None)


def mark_mapped_enum_params(parameters, enums):
    """Mark enum paramaters as mapped-enum if the enum has mappings."""
    for param in (p for p in parameters if "enum" in p):
        enum_name = param["enum"]
        enum = enums[enum_name]
        if "generate-mappings" in enum:
            del param["enum"]
            param["mapped-enum"] = enum_name


def populate_grpc_types(parameters, config):
    """Set the grpc_type of each parameter that doesn't already have it set."""
    for parameter in parameters:
        if "callback_params" in parameter:
            populate_grpc_types(parameter["callback_params"], config)
        if "grpc_type" in parameter:
            continue
        parameter["grpc_type"] = common_helpers.get_grpc_type(parameter["type"], config)


def _get_short_enum_type_name(typename: str) -> str:
    if typename in ["char[]", "const char[]", "char"]:
        return "String"
    stripped_name = common_helpers.strip_prefix(typename, "Vi")
    return common_helpers.ensure_pascal_case(stripped_name)


def _remove_leading_group_name(enum_value_name, group_name):
    return common_helpers.strip_prefix(enum_value_name, f"{group_name.upper()}_")


def _add_leading_enum_name(enum_value_name, enum_name, enum):
    enum_value_prefix = enum.get(
        "enum-value-prefix", common_helpers.pascal_to_snake(enum_name).upper()
    )
    return f"{enum_value_prefix}_{enum_value_name}"


def _add_attribute_values_enums(enums, attribute_enums_by_type, group_name):
    """Add new enums to enums metadata, for use as the value parameter of SetAttribute APIs."""
    for type_name in attribute_enums_by_type:
        unmapped_values = {}
        mapped_values = {}
        for enum_name in sorted(attribute_enums_by_type[type_name]):
            enum = enums[enum_name]
            is_mapped_enum = enum.get("generate-mappings", False)
            for value in enum["values"]:
                # Remove the leading group name (if any) because it will be redundant in the
                # aggregate enum.
                value_name = _remove_leading_group_name(value["name"], group_name)
                # Add a leading enum to differentiate sub-enums within the aggregate values enum.
                value_name = _add_leading_enum_name(value_name, enum_name, enum)
                if is_mapped_enum:
                    mapped_values[value_name] = value["value"]
                else:
                    unmapped_values[value_name] = value["value"]

        shortened_type_name = _get_short_enum_type_name(type_name)
        enum_value_prefix = (f"{group_name}_{shortened_type_name}").upper()
        unmapped_enum_name = _get_attribute_values_enum_name(group_name, type_name)
        mapped_enum_name = _get_attribute_values_enum_name(group_name, type_name, is_mapped=True)
        _add_enum(unmapped_enum_name, unmapped_values, enums, enum_value_prefix)
        _add_enum(mapped_enum_name, mapped_values, enums, enum_value_prefix, is_mapped=True)


AttributeReferencingParameter = namedtuple(
    "AttributeReferencingParameter", ["attribute_group", "parameter"]
)


class AttributeAccessorExpander:
    """Wraps an _attribute_type_map of: group -> type -> attributes.

    Also implements the metadata_mutations to _add_attribute_values_enums,
    _expand_attribute_function_value_param, and patch_attribute_enum_type.
    """

    def __init__(self, metadata):  # noqa: D107
        self._config = metadata["config"]
        self._enums = metadata["enums"]
        self._attribute_type_map = {}

        for group in common_helpers.get_attribute_groups(metadata):
            attribute_enums_by_type = common_helpers.get_attribute_enums_by_type(group.attributes)
            _add_attribute_values_enums(self._enums, attribute_enums_by_type, group.name)
            self._attribute_type_map[group.name] = attribute_enums_by_type

    def _get_attribute_reference_parameter(
        self, parameters
    ) -> Optional[AttributeReferencingParameter]:
        """Get the parameter that references an attribute, if there is one."""

        def try_resolve_attribute_reference(parameter) -> Optional[AttributeReferencingParameter]:
            param_type = parameter["grpc_type"]
            # All attribute parameters must have a grpc_type of {group_name}Attributes.
            # In MI, this is added during metadata mutation of ViAttr types.
            attr_suffix = common_helpers.get_attribute_enum_suffix(self._config)
            potential_attribute_name = common_helpers.strip_suffix(param_type, attr_suffix)
            if potential_attribute_name in self._attribute_type_map:
                return AttributeReferencingParameter(potential_attribute_name, parameter)
            return None

        # Assumption: there are 0 or 1 parameters that reference an attribute per function.
        attribute_resolve_results = (try_resolve_attribute_reference(p) for p in parameters)
        valid_references = (ref for ref in attribute_resolve_results if ref)
        return next(valid_references, None)

    def patch_attribute_enum_type(self, function_name, func):
        """Update the attribute parameter to point to the proper enum name.

        Only applies for drivers that splits attribute enums by type.

        For example, DAQmx has a Get/Set/ResetBufferAttributeUInt32 functions. The parameter that
        selects _which_ attribute needs to use the proper attribute enum. The input says that the
        grpc_type is BufferAttribute, but the generated code splits the enums by type, with a
        special group for Reset functions. So in this example, the Get and Set functions' attribute
        enum would be BufferUInt32Attribute, and the Reset function's attribute enum would be
        BufferResetAttribute.
        """
        if not common_helpers.get_split_attributes_by_type(self._config):
            return
        parameters = func["parameters"]
        attribute_reference = self._get_attribute_reference_parameter(parameters)
        if attribute_reference:
            group = attribute_reference.attribute_group
            attribute_param = attribute_reference.parameter
            if function_name.startswith("Reset"):
                sub_group = "Reset"
            else:
                value_param = _get_attribute_function_value_param(func)
                sub_group = common_helpers.get_grpc_type_name_for_identifier(
                    value_param["type"], self._config
                )
            if common_helpers.supports_raw_attributes(self._config):
                attribute_param["enum"] = common_helpers.get_attribute_enum_name(
                    group, sub_group, self._config
                )
                attribute_param["grpc_type"] = "int32"
                attribute_param["raw_attribute"] = True
            else:
                attribute_param["grpc_type"] = common_helpers.get_attribute_enum_name(
                    group, sub_group, self._config
                )

    def expand_attribute_value_params(self, func):
        """Update the attrVal parameter to use the proper enum, if available."""
        parameters = func["parameters"]
        attribute_reference = self._get_attribute_reference_parameter(parameters)
        if attribute_reference:
            _expand_attribute_function_value_param(
                func,
                self._enums,
                self._attribute_type_map[attribute_reference.attribute_group],
                attribute_reference.attribute_group,
            )


def _expand_attribute_function_value_param(
    function, enums, attribute_enums_by_type, service_class_prefix
):
    """For SetAttribute and CheckAttribute, update function metadata to mark value param as enum."""
    value_param = _get_attribute_function_value_param(function)
    if not value_param:
        return
    if value_param["direction"] == "out":
        if common_helpers.is_static_castable_enum_type(value_param):
            value_param["use_checked_enum_conversion"] = True
        else:
            return

    if value_param["type"] == "ViConstString":
        param_type = "ViString"
    else:
        param_type = common_helpers.get_underlying_type(value_param)
    if param_type in attribute_enums_by_type:
        enum_name = _get_attribute_values_enum_name(service_class_prefix, param_type)
        mapped_enum_name = _get_attribute_values_enum_name(
            service_class_prefix, param_type, is_mapped=True
        )
        enum_exists = enum_name in enums
        mapped_enum_exists = mapped_enum_name in enums
        if enum_exists:
            value_param["enum"] = enum_name
        if mapped_enum_exists:
            value_param["mapped-enum"] = mapped_enum_name
        if not enum_exists and not mapped_enum_exists:
            # Ideally there must be an enum associated with this parameter for SetAttribute* API.
            # Since the enum is empty, users will be passing in raw values. Make it clear via
            # naming.
            value_param["name"] += "_raw"


def _get_attribute_function_value_param(function):
    return next(
        (
            param
            for param in function["parameters"]
            if param["name"] in {"value", "attributeValue", "attrVal"}
        ),
        None,
    )


def _get_attribute_values_enum_name(group_name, type, is_mapped=False):
    enum_name_suffix = "Mapped" if is_mapped else ""
    shortened_type_name = _get_short_enum_type_name(type)
    return group_name + shortened_type_name + "AttributeValues" + enum_name_suffix


def _add_enum(enum_name, enum_values, enums, enum_value_prefix, is_mapped=False):
    if not enum_values:
        return
    values = [{"name": name, "value": enum_values[name]} for name in enum_values]
    new_enum = {
        "enum-value-prefix": enum_value_prefix,
        "generate-mappings": is_mapped,
        "values": values,
    }
    enums.update({enum_name: new_enum})


def move_zero_enums_to_front(enums: dict) -> None:
    """Put the enum value with value 0 _first_ if there is one, or else add UNSPECIFIED enum value.

    protobuf requires that the first enum value be zero. For enums missing a zero value,
    we will add an UNSPECIFIED enum value to the front (this is the best practice). But if
    there already is a zero enum, make sure that pre-existing zero value is at the front.
    """
    for enum in enums.values():
        values = enum["values"]
        try:
            i, _ = next((i, v) for i, v in enumerate(values) if v["value"] == 0)
            values.insert(0, values.pop(i))
        except StopIteration:
            pass

Metadata mutation. # These are all of the coerced narrow numerics used in RFmx and DAQmx. # This will not account for other aliases of narrow numerics. # Note that params can also be marked coerced instead of updating this list. # uint8 is not included because it can be represented as a byte. Sanitize name fields on a list of parameter objects.      Also, populate the cppname field with the sanitized value. Set information about varargs parameters in the metadata. Mark the size parameters in the metadata. Mark parameters with narrow numeric types as coerced. Mark the parameters that shouldn't be included in the proto request message.      Their values should be derived from other sources in the service handlers. # Output size_params can still be included in the proto # as information. Mark enum paramaters as mapped-enum if the enum has mappings. Set the grpc_type of each parameter that doesn't already have it set. Add new enums to enums metadata, for use as the value parameter of SetAttribute APIs. # Remove the leading group name (if any) because it will be redundant in the # aggregate enum. # Add a leading enum to differentiate sub-enums within the aggregate values enum. Wraps an _attribute_type_map of: group -> type -> attributes.      Also implements the metadata_mutations to _add_attribute_values_enums,     _expand_attribute_function_value_param, and patch_attribute_enum_type. # noqa: D107 Get the parameter that references an attribute, if there is one. # All attribute parameters must have a grpc_type of {group_name}Attributes. # In MI, this is added during metadata mutation of ViAttr types. # Assumption: there are 0 or 1 parameters that reference an attribute per function. Update the attribute parameter to point to the proper enum name.          Only applies for drivers that splits attribute enums by type.          For example, DAQmx has a Get/Set/ResetBufferAttributeUInt32 functions. The parameter that         selects _which_ attribute needs to use the proper attribute enum. The input says that the         grpc_type is BufferAttribute, but the generated code splits the enums by type, with a         special group for Reset functions. So in this example, the Get and Set functions' attribute         enum would be BufferUInt32Attribute, and the Reset function's attribute enum would be         BufferResetAttribute. Update the attrVal parameter to use the proper enum, if available. For SetAttribute and CheckAttribute, update function metadata to mark value param as enum. # Ideally there must be an enum associated with this parameter for SetAttribute* API. # Since the enum is empty, users will be passing in raw values. Make it clear via # naming. Put the enum value with value 0 _first_ if there is one, or else add UNSPECIFIED enum value.      protobuf requires that the first enum value be zero. For enums missing a zero value,     we will add an UNSPECIFIED enum value to the front (this is the best practice). But if     there already is a zero enum, make sure that pre-existing zero value is at the front.