{% from "macros.j2" import cls_name,pointer,super,argtypes,argnames,method_pointer,
static_method_pointer,function_pointer,template_args_typename,template_args,
template_method_pointer,template_static_method_pointer, template_pointer,
prototype, pybind_overload, argtypes_names, argnames_wo_type, argnames_byref,
argtypes_names_not_byref, init_outputs_byref, argnames_not_byref, trampoline_class,
handle_results_ptr_byref %}

// std lib related includes
#include <tuple>

// pybind 11 related includes
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

namespace py = pybind11;

// Standard Handle
#include <Standard_Handle.hxx>

{% if module_settings['include_header_pre_top'] %}
{{ module_settings['include_header_pre_top'] }}
{% endif %}

// includes to resolve forward declarations
{% for h in module.headers %}{% for f in h.forwards %}{% if f.name in class_dict %}
#include <{{ class_dict[f.name].splitpath()[-1] }}>
{% endif %}{% endfor %}{% endfor %}

// module includes
{% for h in module.headers | sort(attribute='short_name') %}
#include <{{ h.short_name }}>
{% endfor %}

// template related includes
{% for h in module.headers %}
{% for td in h.typedefs %}
{% if not td.pod and not "_H" in td.name and td.template_base.__len__()>0
    and not td.type.startswith("opencascade::handle")
    and not td.type.startswith("std::") %}

// {{h.name}}
{% set dep_module = td.type.split("_") %}
#include "{{dep_module[0]}}_tmpl.hxx"
{% endif %}
{% endfor %}
{% endfor %}


// user-defined pre
{% if include_pre %}
#include "{{ include_pre }}"
{% endif %}

// user-defined inclusion per module
{% if module_settings['include_header_pre'] %}
{{ module_settings['include_header_pre'] }}
{% endif %}

// Module definiiton
void register_{{module.name}}(py::module &main_module) {


py::module m = static_cast<py::module>(main_module.attr("{{module.name}}"));
py::object klass;

//Python trampoline classes
{% for c in module.classes %}{% if c.abstract %}
    {{ trampoline_class(c) }}
{% endif %}{% endfor %}

// classes
{% for c in module.classes %}{% if (proper_new_operator(c) and proper_delete_operator(c)) or 'Standard_Transient' in  c.rootclass %}

    // Class {{c.name}} from {{ module.class_dict[c.name] }}
    klass = m.attr("{{cls_name(c)}}");

    {% if not c.constructors_unfiltered and not c.nonpublic_constructors and not c.abstract %}
    // default constructor
    register_default_constructor<{{c.name}} {{pointer(c)}}>(m,"{{cls_name(c)}}");
    {% endif %}

    // nested enums
    {% for enum in c.enums %}
    {% if enum.anonymous %}
        {% for val in enum.values %}
        klass.attr("{{ val }}") = py::cast(int({{ enum.name+"::" if enum.name}}{{ val }}));
        {% endfor %}
    {% else %}
        py::enum_<{{enum.name}}>(klass, "{{ enum.name.split('::')[-1] + '_e' }}", R"#({{enum.comment}})#")
        {% for val in enum.values %}
            .value("{{val}}", {{enum.name}}::{{val}}){{ ".export_values();" if loop.last }}
        {% endfor %}
    {% endif %}
    {% endfor %}

    static_cast<py::class_<{{c.name}} {{pointer(c)}} {% if c.abstract %},Py_{{c.name}}{% endif %} {{super(c,class_dict,module.typedef_dict)}}>>(klass)
    // constructors
    {% for i,con in enumerate(c.constructors) %}
    {% if (not con.pointer_by_ref) and
          (not i in module_settings['Classes'].get(c.name,{}).get('exclude_constructors',[]))  %}
        .def(py::init< {{ argtypes(con) }} >() {{argnames(con)}} )
    {% endif %}
    {% endfor %}
    // custom constructors
    {% for con in module_settings['Classes'].get(c.name,{}).get('additional_constructors',[]) %}
        .def(py::init( {{con.body}} ))
    {% endfor %}
    // methods
    {% for m in c.methods %}
    {% if not m.pointer_by_ref %}
        .def("{{m.name}}",
             {{ method_pointer(c,m) }},
             R"#({{m.comment}})#" {{argnames(m)}}
          )
    {% endif %}
    {% endfor %}
    // methods using call by reference i.s.o. return
    {% for m in c.methods_byref %}
    {% if not m.pointer_by_ref %}
        .def("{{m.name}}",
             []( {{c.name}} &self {{ "," if argtypes_names_not_byref(m)|length}} {{argtypes_names_not_byref(m)}} ){
                 {{init_outputs_byref(m) | indent(16) }}
                 self.{{m.name}}({{argnames_wo_type(m)}});
                 {{handle_results_ptr_byref(m) | indent(16) }}
                 return std::make_tuple({{argnames_byref(m)}}); },
             R"#({{m.comment}})#" {{argnames_not_byref(m)}}
          )
    {% endif %}
    {% endfor %}
    // static methods
    {% for m in c.static_methods %}
    {% if not m.pointer_by_ref %}
        .def_static("{{m.name}}_s",
                    {{ static_method_pointer(c,m) }},
                    R"#({{m.comment}})#" {{argnames(m)}}
          )
    {% endif %}
    {% endfor %}
    // static methods using call by reference i.s.o. return
    {% for m in c.static_methods_byref %}
    {% if not m.pointer_by_ref %}
        .def_static("{{m.name}}_s",
            []({{argtypes_names_not_byref(m)}} ){
                {{init_outputs_byref(m) | indent(16) }}
                {{c.name}}::{{m.name}}({{argnames_wo_type(m)}});
                {{handle_results_ptr_byref(m) | indent(16) }}
                {% if args_byref(m) %}return std::make_tuple({{argnames_byref(m)}});{% endif %} },
            R"#({{m.comment}})#" {{argnames_not_byref(m)}}
          )
    {% endif %}
    {% endfor %}
    // operators
    {% for op in c.operators %}
    {% if op.name in operator_dict %}
    {% for py_op in operator_dict[op.name] %}
        .def("{{py_op}}",
             {{ method_pointer(c,op) }},
             py::is_operator(),
             R"#({{op.comment}})#" {{argnames(op)}}
          )
    {% endfor %}
    {% endif %}
    {% endfor %}
    // additional methods and static methods
    {% if c.name in module_settings['Classes'] %}
    {% for name,m in module_settings['Classes'][c.name]['additional_methods'].items() %}
        .def("{{ name}}",
             {{ m['body']}},
             R"#({{ m['help'] }})#"
             {% for arg in m['arguments'] %}, py::arg("{{ arg }}"){% endfor %}{% if m['return_policy'] %}, {{ m['return_policy'] }} {% endif %}
          )
    {% endfor %}
    {% for name,m in module_settings['Classes'][c.name]['additional_static_methods'].items() %}
        .def_static("{{ name}}",
                    {{ m['body']}},
                    R"#({{ m['help'] }})#"
                    {% for arg in m['arguments'] %}, py::arg("{{ arg }}"){% endfor %}{% if m['return_policy'] %}, {{ m['return_policy'] }} {% endif %}
          )
    {% endfor %}
    {% endif %}
    // properties
    {% for f in c.fields if f.type in all_classes or f.type in all_enums or f.type in all_typedefs or f.type in settings['byref_types'] %}
    {% if f.const %}
        .def_readonly("{{f.name}}", &{{c.name}}::{{f.name}})
    {% else %}
        .def_readwrite("{{f.name}}", &{{c.name}}::{{f.name}})
    {% endif %}
    {% endfor %}
    // methods returning by ref wrapped as properties
    {% for m in c.methods_return_byref %}
    {% if m.return_type.replace('&','').strip() in settings['byref_types'] %}
       .def_property("{{m.name}}",
                     []({{c.name}}& self){return self.{{m.name}}();} {% if not  m.return_type.startswith('const') %},
                     []({{c.name}}& self, {{m.return_type.replace('&','')}} val){self.{{m.name}}() = val;}, {% endif %}
                     R"#({{m.comment}})#"
         )
    {% else %}
       .def("{{m.name}}",
             {{ method_pointer(c,m) }},
             R"#({{m.comment}})#"
             {{argnames(m)}}
             {% if (not is_byref_smart_ptr(m.return_type) and not m.return_type.strip().startswith('const'))
                   or "TColgp_" in m.return_type or "TColStd_" in m.return_type %}
             , py::return_value_policy::reference_internal
             {% endif %}
         )
    {% endif %}
    {%- endfor -%}
    ;
{% endif %}{% endfor %}

// functions
{% for ns in module.namespaces %}
auto m{{ns}} = static_cast<py::module>(m.attr("{{ns}}"));
{% endfor %}

{% for h in module.headers %}
// {{h.name}}
{% for f in h.functions %}
    {{ "m" + (f.namespace or "") }}.def("{{f.name}}",
          {{ function_pointer(f) }},
          R"#({{ f.comment }})#" {{argnames(f)}}
          );
{% endfor %}
{% endfor %}

// Additional functions

{% for name,m in module_settings['additional_functions'].items() %}
    m.def("{{name}}",
          {{ m['body']}},
          R"#({{ m['help'] }})#"
          {% for arg in m['arguments'] %}, py::arg("{{ arg }}"){% endfor %}
          )
{% endfor %}

// operators
{% for op in module.operators %}
{% if op.name in operator_dict %}
{% for py_op in operator_dict[op.name] %}
    m.def("{{py_op}}",
          {{ function_pointer(op) }},
          py::is_operator(),
          R"#({{ op.comment }})#" {{argnames(op)}});
{% endfor %}
{% endif %}
{% endfor %}

// register typdefs
{% for h in module.headers %}
{% for td in h.typedefs %}
{% if not td.pod and not "_H" in td.name and td.template_base.__len__()>0 and not td.type.startswith("opencascade::handle") %}
{% set dep_module = td.template_base[0].split("_") %}
{% set arg = td.type.split(td.template_base[0])[-1] %}
{% set base =  td.template_base[0] %}
{% if not arg.endswith("::Iterator") %}
    register_template_{{ base }}{{ arg }}(m,"{{td.name}}");
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}


// exceptions
{% for ex in module.exceptions %}
register_occ_exception<{{ex.name}}>(m, "{{ex.name}}");
{% endfor %}

// user-defined post-inclusion per module in the body
{% if module_settings['include_body_post'] %}
{{ module_settings['include_body_post'] }}
{% endif %}

};

// user-defined post-inclusion per module
{% if module_settings['include_header_post'] %}
{{ module_settings['include_header_post'] }}
{% endif %}

// user-defined post
{% if include_post %}
#include "{{ include_post }}"
{% endif %}
