{# loop over all functions and create types for each #}
{%- for name, function_data in functions.items() -%}

{% set contract_function_class_name = contract_name+function_data.capitalized_name+"ContractFunction" %}

{# go through signatures, create a class per signature #}
{% for i in range(function_data.signature_datas|length) %}

class {{contract_function_class_name}}{{i}}(PypechainContractFunction):
    """ContractFunction for the {{function_data.name}}({{",".join(function_data.signature_datas[i].input_types)}}) method."""

    _function_name = "{{function_data.name}}"
    _type_signature = expand_struct_type_str(
        tuple({{function_data.signature_datas[i].input_types}}),
        structs
    )
    _error_class = {{contract_name}}ContractErrors

    {% set output_names = function_data.signature_datas[i].outputs %}
    {% set output_types = function_data.signature_datas[i].output_types %}
    {% set return_type = output_types[0] if output_types|length == 1 else "ReturnValues" if output_types|length > 1 else "None" %}

    {% if output_types|length > 1%}
    class ReturnValues(NamedTuple):
        """The return named tuple for {{function_data.capitalized_name}}."""
        {% for j in range(output_types | length) -%}
            {{output_names[j]}}: {{output_types[j]}}
        {% endfor %}
    {% endif %}

    def call(
        self,
        transaction: TxParams | None = None,
        block_identifier: BlockIdentifier | None = None,
        state_override: StateOverride | None = None,
        ccip_read_enabled: bool | None = None,
    ) -> {{return_type}}:
        """returns {{return_type}}."""
        # We handle the block identifier = None case here for typing.
        if block_identifier is None:
            block_identifier = self.w3.eth.default_block

        {% if output_types|length == 1 %}
        # Define the expected return types from the smart contract call
        return_types = {{return_type}}
        {% elif output_types|length > 1%}
        # Define the expected return types from the smart contract call
        return_types = [{{output_types|join(', ')}}]
        {% endif %}

        # Call the function
        raw_values = self._call(transaction, block_identifier, state_override, ccip_read_enabled)

        {% if output_types|length == 1 %}
        return cast({{output_types[0]}}, rename_returned_types(structs, return_types, raw_values))
        {% elif output_types|length > 1%}
        return self.{{return_type}}(*rename_returned_types(structs, return_types, raw_values))
        {% endif %}

{% endfor %}
class {{contract_function_class_name}}(PypechainOverloadedFunctions):
    """ContractFunction for the {{function_data.name}} method."""
    # super() call methods are generic, while our version adds values & types
    # pylint: disable=arguments-differ# disable this warning when there is overloading
    # pylint: disable=function-redefined

    _function_name = "{{function_data.name}}"


    # Make lookup for function signature -> overloaded function
    # The function signatures are python types, as we need to do a
    # lookup of arguments passed in to contract function
    _functions: dict[str, PypechainContractFunction]

{# go through signatures, create a class per signature #}
{% for i in range(function_data.signature_datas|length) %}
    @overload
    def __call__(self{% if function_data.signature_datas[i].input_names_and_types %}, {{function_data.signature_datas[i].input_names_and_types|join(', ')}}{% endif %}) -> {{contract_function_class_name}}{{i}}:  # type: ignore
        ...
{% endfor %}

    def __call__(self, *args, **kwargs) -> {{contract_function_class_name}}:  # type: ignore
        # Special case when there are no args or kwargs
        if len(args) == 0 and len(kwargs) == 0:
            # We need to specify the element identifier as the function call without arguments.
            # Despite this setting the member variable `abi_element_identifier`
            # that's shared across this object, this field gets overwritten in the
            # clone if arguments are provided.
            self.abi_element_identifier = self._function_name + "()"
            clone = super().__call__()
        else:
            clone = super().__call__(*(dataclass_to_tuple(arg) for arg in args), **{key: dataclass_to_tuple(arg) for key, arg in kwargs.items()})

        # Arguments is the flattened set of arguments from args and kwargs, ordered by the abi
        # We get the python types of the args passed in, but remapped from tuples -> dataclasses
        arg_types = get_arg_type_names(clone.arguments)

        # Grab the relevant kwargs when factory was called.
        factory_kwargs = self._factory_kwargs
        factory_kwargs["abi_element_identifier"] = clone.abi_element_identifier

        function_obj = self._overloaded_functions[arg_types].factory(
            self._function_name,
            **factory_kwargs
        )

        function_obj.args = clone.args
        function_obj.kwargs = clone.kwargs

        # The `@overload` of `__call__` takes care of setting the type of this object correctly
        return function_obj # type: ignore
    
    @classmethod
    def factory(
        cls, class_name: str, **kwargs: Any
    ) -> Self:
        out = super().factory(class_name, **kwargs)
        # Store the factory args for downstream consumption
        out._factory_kwargs = kwargs

        # We initialize our overridden functions here.
        # Note that we use the initialized object to ensure each function
        # is attached to the instantiated object 
        # (attached to a specific web3 and contract address)
        out._overloaded_functions = {
        {% for i in range(function_data.signature_datas|length) %}
            {{contract_function_class_name}}{{i}}._type_signature: {{contract_function_class_name}}{{i}},
        {% endfor %}
        }
        return out



{% endfor %}

class {{contract_name}}ContractFunctions(ContractFunctions):
    """ContractFunctions for the {{contract_name}} contract."""
{% for function in functions.values() %}
    {{function.name}}: {{contract_name}}{{function.capitalized_name}}ContractFunction
{% endfor %}
    def __init__(
         self,
         abi: ABI,
         w3: "Web3",
         address: ChecksumAddress | None = None,
         decode_tuples: bool | None = False,
     ) -> None:
         super().__init__(abi, w3, address, decode_tuples)
         {% for function in functions.values() -%}
         self.{{function.name}} = {{contract_name}}{{function.capitalized_name}}ContractFunction.factory(
             "{{function.name}}",
             w3=w3,
             contract_abi=abi,
             address=address,
             decode_tuples=decode_tuples,
             abi_element_identifier="{{function.name}}",
         )
         {% endfor %}
