{%- macro genbase(cls) -%}
  
{%- endmacro -%}

{%- macro fndef(cls_qualname, fn) %}
{% if cls_qualname and fn.static %}def_static{% else %}def{% endif %}
{% endmacro -%}

{%- macro fnptr(cls_qualname, fn, trampoline_qualname) -%}
  {%- if fn.data.cpp_code -%}
    {{ fn.data.cpp_code }}
  {%- elif not fn.x_genlambda -%}
    &
    {%- if trampoline_qualname -%}
      {{ trampoline_qualname }}::
    {%- elif cls_qualname -%}
      {{ cls_qualname }}::
    {%- else -%}
      {{ fn.namespace }}
    {%- endif -%}
    {{ fn.name }}
  {%- else -%}
    [](
      {%- if cls_qualname -%}
        {{ cls_qualname }} * __that
        {%- if fn.x_in_params %},{% endif -%}
      {% endif -%}
      {{ fn.x_in_params | join(', ', attribute='x_decl') -}}
      ) {
          {% for stmt in fn.x_lambda_pre %}
          {{ stmt }};
          {% endfor %}
          {{ fn.x_callstart }}
          {%- if trampoline_qualname -%}
            (({{ trampoline_qualname }}*)__that)->
          {%- elif cls_qualname -%}
            __that->
          {%- else -%}
            {{ fn.namespace }}
          {%- endif -%}
          {{ fn.name }}({{ fn.parameters | join(', ', attribute='x_callname') }} {{ fn.x_callend }});
          {% for stmt in fn.x_lambda_post %}
          {{ stmt }};
          {% endfor %}
          {{ fn.x_wrap_return }}
        }
  {%- endif -%}
{%- endmacro -%}

{%- macro gensig(cls_qualname, fn) -%}
  {#-
    py::overload_cast fails in some obscure cases, so we don't use it here
    https://github.com/pybind/pybind11/issues/1153
  -#}
  {{- fn.rtnType }} (
  {%- if cls_qualname and not fn.static -%}
    {{ cls_qualname }}::
  {%- endif -%}
  *)(
      {{- fn.x_in_params | join(', ', attribute='x_type_full') -}}
  )
  {%- if fn.const %} const{% endif -%}
{%- endmacro -%}

{%- macro genmethod(cls_qualname, fn, trampoline_qualname) -%}
  {%- set ns = namespace(qualname=cls_qualname, arg_params=fn.x_in_params) -%}
  {%+ if fn.data.ifdef %}

  #ifdef {{ fn.data.ifdef }}
  {% endif %}
  {%+ if fn.data.ifndef %}

  #ifndef {{ fn.data.ifndef }}
  {% endif %}
  {%- if fn.constructor -%}
    {%- set ns.arg_params = fn.x_all_params -%}
    {%- if fn.data.cpp_code -%}
      {{ fn.data.cpp_code }}
    {%- elif trampoline_qualname -%}
      .def(py::init([]({{ ns.arg_params | join(', ', attribute='x_decl') }}) {
        return std::make_unique<{{ trampoline_qualname }}>(
          {{- ns.arg_params | join(', ', attribute='x_callname') -}}
        );
      })
    {%- else -%}
      .def(py::init<{{ ns.arg_params | join(', ', attribute='x_type_full') }}>()
    {%- endif -%}
  {%- else -%}
    .{{ fndef(cls_qualname, fn) }}("{{ fn.x_name }}",{{ ' ' }}
      {%- if not fn.data.cpp_code and not fn.x_genlambda -%}
        
        {%- if trampoline_qualname -%}
          {%- set ns.qualname = trampoline_qualname -%}
          static_cast<{{ gensig(cls_qualname, fn) }}>(
        {%- endif -%}
        {%- if fn.x_overloaded -%}
          static_cast<{{ gensig(ns.qualname, fn) }}>(
        {% endif -%}
      {%- endif -%}
	      {{- fnptr(cls_qualname, fn, trampoline_qualname) -}}
      {%- if not fn.data.cpp_code and not fn.x_genlambda -%}
        {%- if fn.x_overloaded -%}){%- endif -%}
        {%- if trampoline_qualname -%}){%- endif -%}
      {%- endif -%}
  {%- endif -%}

  {%- if ns.arg_params -%},
      {{ ns.arg_params | join(', ', attribute='x_pyarg') }}
  {%- endif -%}

  {%- if not fn.data.no_release_gil -%}
    , release_gil()
  {%- endif -%}

  {%- for nurse, patient in fn.x_keepalives %}
    , py::keep_alive<{{ nurse }}, {{ patient }}>()
  {%- endfor -%}

  {{- fn.x_return_value_policy -}}

  {%- if fn.x_doc %},
    {% for dq in fn.x_doc_quoted %}
      {{ dq }}{% if loop.nextitem is defined %}{{ '\n' }}{% endif %}
    {%- endfor -%}
  {%- endif -%}
  )
  {%+ if fn.data.ifdef %}
  #endif // {{ fn.data.ifdef }}
  {% endif %}
  {%+ if fn.data.ifndef %}
  #endif // {{ fn.data.ifndef }}
  {% endif %}
{%- endmacro -%}

{%- macro genprop(qualname, prop) -%}
  {%- if prop.array_size is defined -%}
    .def_property_readonly("{{ prop.x_name }}", []({{ qualname }}& inst) {
        return py::memoryview(py::buffer_info(
          &inst.{{ prop.name }}, sizeof({{ prop.type }}),
          py::format_descriptor<{{ prop.type }}>::format(),
          1, {{ "{" }}{{ prop.array_size }}{{ "}" }}, {sizeof({{ prop.type }})}
        ));
    })
  {%- elif prop.array -%}
    .def_property("{{ prop.x_name }}",
      [](const {{ qualname }}& inst) { return inst.{{ prop.name}}; },
      []({{ qualname }}& inst, const {{ prop.type }} v) {inst.{{ prop.name}} = v; })
  {%- elif prop.reference -%}
    .def_property
    {%- if prop.constant or prop.constexpr or prop.x_readonly -%}
        _readonly
    {%- endif -%}
    ("{{ prop.x_name }}", {{ ' ' }}
        [](const {{ qualname }}& inst) -> {{ prop.type }} { return inst.{{ prop.name}}; }
        {%- if not (prop.constant or prop.constexpr or prop.x_readonly) %},
        []({{ qualname }}& inst, {{ prop.type }} v) {inst.{{ prop.name}} = v; }
        {%- endif -%}
    )

  {%- else -%}
    .def_
      {%- if prop.constant or prop.constexpr or prop.x_readonly -%}
        readonly
      {%- else -%}
        readwrite
      {%- endif -%}
      {%- if prop.static %}_static{% endif -%}
      ("{{ prop.x_name }}", &{{ qualname }}::{{ prop.name}})
  {%- endif -%}
{%- endmacro -%}

{%- macro genenum(scope, enum) %}
{%- if 'name' in enum -%}
py::enum_<{{ enum.x_namespace }}{{ enum.name }}>({{ scope }}, "{{ enum.x_name }}")
  {% for val in enum['values'] if not val.data.ignore %}
    .value("{{ val.x_name }}", {{ enum.x_namespace }}{{ enum.name }}::{{ val.name }})
  {% endfor -%}
  ;
{%- endif -%}
{% endmacro -%}

{%- macro unnamed_enum(x, enums) %}
{% for enum in enums if 'name' not in enum -%}
  {% for val in enum['values'] -%}
    {{ x }}.attr("{{ val.name }}") = (int){{ enum.x_namespace }}{{ val.name }};
  {% endfor %}
{% endfor %}
{% endmacro -%}

// This file is autogenerated. DO NOT EDIT
#include <robotpy_build.h>

{% for inc in data.extra_includes_first %}
#include <{{ inc }}>
{% endfor %}

#include <{{ header.rel_fname }}>

{% for inc in type_caster_includes %}
#include <{{ inc }}>
{% endfor %}

{% for using in header.using.values() if using.using_type != "typealias" %}
using {{ using.raw_type }};
{% endfor %}

{% for cls in header.classes
   if not cls.data.ignore and cls.x_has_trampoline %}
#define RPYGEN_ENABLE_{{ cls.x_qualname_ }}_PROTECTED_CONSTRUCTORS
#include <rpygen/{{ cls.x_qualname_ }}.hpp>
{% endfor %}

{% for inc in data.extra_includes %}
#include <{{ inc }}>
{% endfor %}

{#
  Ordering of the initialization function

  - namespace/typealiases
  - global enums
  - class declarations
  - class enums
  - class methods
  - global methods
#}

void init_{{ mod_fn }}(py::module &m) {

{# namespaces/typealiases #}
{% for cls in header.classes if not cls.data.ignore %}
  {% if cls.parent %}
    using {{ cls.name }} = {{ cls.parent.name }}::{{ cls.name }};
  {% elif cls.namespace %}
    using namespace {{ cls.namespace }};
  {% endif %}
  {% for typealias in cls.data.typealias -%}
    using {{ typealias.split('::')[-1] }} = {{ typealias }};
  {% endfor %}
  {% for constant in cls.data.constants -%}
    static constexpr auto {{ constant.split('::')[-1] }} = {{ constant }};
  {% endfor %}
{% endfor %}

{# subpackages #}
{% for pkg, vname in subpackages.items() %}
  auto {{ vname }} = m.def_submodule("{{ pkg }}");
{% endfor %}

{# define global enums in case they are used as default args #}  
{% for enum in header.enums %}
  {{ genenum(enum.x_module_var, enum) }}
{% endfor %}

{# class declarations #}
{% for cls in header.classes if not cls.data.ignore %}
  py::class_<{{ cls.name }}
    {%- if cls.data.nodelete -%}
      , std::unique_ptr<{{ cls.name }}, py::nodelete>
    {%- elif cls.data.shared_ptr -%}
      , std::shared_ptr<{{ cls.name }}>
    {%- endif -%}
    {%- if cls.x_has_trampoline -%}
      , {{ cls.x_trampoline_name }}
    {%- endif -%}

    {%- if cls.x_inherits -%}
      , {{ cls.x_inherits | join(', ', attribute='class') }}
    {%- endif -%}
    
    >
      {{ cls.x_varname }}(

    {%- if cls.parent -%}
      {{ cls.parent.x_varname }}
    {%- else -%}
      {{ cls.x_module_var }}
    {%- endif -%}
    , "{{ cls.x_name }}");
  
  {# define class enums in case they are used as default args #}
  {% for enum in cls.enums.public -%}
    {{ genenum(cls.x_varname, enum) }}
  {% endfor -%}

{% endfor %}

{# class methods #}
{% for cls in header.classes if not cls.data.ignore %}
  {{ cls.x_varname }}
  {# default constructor if not defined #}
  {% if not cls.x_has_constructor and not cls.data.nodelete and not cls.data.force_no_default_constructor %}
    .def(py::init<>(), release_gil())
  {% endif -%}
  {%- for fn in cls.methods.public if not fn.data.ignore %}
    {{ genmethod(cls.x_qualname, fn, None) }}
  {% endfor -%}

  {%- if cls.x_has_trampoline -%}
  {%- for fn in cls.methods.protected if not fn.data.ignore %}
    {{ genmethod(cls.x_qualname, fn, cls.x_trampoline_name) }}
  {% endfor -%}
  {%- endif -%}

  {%- for prop in cls.properties.public if not prop.data.ignore %}
    {{ genprop(cls.x_qualname, prop) }}
  {% endfor -%}
  {%- for prop in cls.properties.protected if not prop.data.ignore %}
    {{ genprop(cls.x_trampoline_name, prop) }}
  {%- endfor %};

  {{ unnamed_enum(cls.x_varname, cls.enums.public) }}
{% endfor %}

{# global methods #}
{% if header.functions %}
  {% for fn in header.functions if not fn.data.ignore -%}
    {{ fn.x_module_var }}{{ genmethod(None, fn, None) }};
  {% endfor %};
{% endif %}
{% if data.inline_code %}

  {{ data.inline_code }}
{% endif %}
}
