{#     Copyright 2023, Kay Hayen, mailto:kay.hayen@gmail.com                    #}
{#                                                                              #}
{#     Part of "Nuitka", an optimizing Python compiler that is compatible and   #}
{#     integrates with CPython, but also works on its own.                      #}
{#                                                                              #}
{#     Licensed under the Apache License, Version 2.0 (the "License");          #}
{#     you may not use this file except in compliance with the License.         #}
{#     You may obtain a copy of the License at                                  #}
{#                                                                              #}
{#        http://www.apache.org/licenses/LICENSE-2.0                            #}
{#                                                                              #}
{#     Unless required by applicable law or agreed to in writing, software      #}
{#     distributed under the License is distributed on an "AS IS" BASIS,        #}
{#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #}
{#     See the License for the specific language governing permissions and      #}
{#     limitations under the License.                                           #}
{#                                                                              #}
{% from 'HelperObjectTools.c.j2' import CHECK_OBJECTS %}
{% macro call_function_with_args(called, args, args_count) %}
{% if args_count == 0 %}
CALL_FUNCTION_NO_ARGS(tstate, {{ called }})
{% elif args_count == 1 %}
CALL_FUNCTION_WITH_SINGLE_ARG(tstate, {{ called }}, {{ args }}[0])
{% else %}
CALL_FUNCTION_WITH_ARGS{{args_count}}(tstate, {{ called }}, {{args}})
{% endif %}
{% endmacro %}
{% if args_count == 0 %}
PyObject *CALL_METHOD_NO_ARGS(PyThreadState *tstate, PyObject *source, PyObject *attr_name) {
{% elif args_count == 1 %}
PyObject *CALL_METHOD_WITH_SINGLE_ARG(PyThreadState *tstate, PyObject *source, PyObject *attr_name, PyObject *arg) {
    PyObject *const *args = &arg; // For easier code compatibility.
{% else %}
PyObject *CALL_METHOD_WITH_ARGS{{args_count}}(PyThreadState *tstate, PyObject *source, PyObject *attr_name, PyObject *const *args) {
{% endif %}
    CHECK_OBJECT(source);
    CHECK_OBJECT(attr_name);

    {{ CHECK_OBJECTS(args, args_count) }}

    PyTypeObject *type = Py_TYPE(source);

    if (hasTypeGenericGetAttr(type)) {
        // Unfortunately this is required, although of cause rarely necessary.
        if (unlikely(type->tp_dict == NULL)) {
            if (unlikely(PyType_Ready(type) < 0)) {
                return NULL;
            }
        }

        PyObject *descr = Nuitka_TypeLookup(type, attr_name);
        descrgetfunc func = NULL;

        if (descr != NULL) {
            Py_INCREF(descr);

            if (NuitkaType_HasFeatureClass(Py_TYPE(descr))) {
                func = Py_TYPE(descr)->tp_descr_get;

                if (func != NULL && PyDescr_IsData(descr)) {
                    PyObject *called_object = func(descr, source, (PyObject *)type);
                    Py_DECREF(descr);

                    PyObject *result = {{ call_function_with_args( "called_object", "args", args_count) }};
                    Py_DECREF(called_object);
                    return result;
                }
            }
        }

        Py_ssize_t dictoffset = type->tp_dictoffset;
        PyObject *dict = NULL;

        if (dictoffset != 0) {
            // Negative dictionary offsets have special meaning.
            if (dictoffset < 0) {
                Py_ssize_t tsize;
                size_t size;

                tsize = ((PyVarObject *)source)->ob_size;
                if (tsize < 0) {
                    tsize = -tsize;
                }
                size = _PyObject_VAR_SIZE(type, tsize);

                dictoffset += (long)size;
            }

            PyObject **dictptr = (PyObject **) ((char *)source + dictoffset);
            dict = *dictptr;
        }

        if (dict != NULL) {
            CHECK_OBJECT(dict);

            Py_INCREF(dict);

            PyObject *called_object = DICT_GET_ITEM1(tstate, dict, attr_name);

            if (called_object != NULL) {
                Py_XDECREF(descr);
                Py_DECREF(dict);

                PyObject *result = {{ call_function_with_args( "called_object", "args", args_count) }};
                Py_DECREF(called_object);
                return result;
            }

            Py_DECREF(dict);
        }

        if (func != NULL) {
            if (func == Nuitka_Function_Type.tp_descr_get) {
{% if args_count != 0 %}
                PyObject *result = Nuitka_CallMethodFunctionPosArgs(
                    tstate,
                    (struct Nuitka_FunctionObject const *)descr,
                    source,
                    args,
                    {{ args_count }}
                );
{% else %}
                PyObject *result = Nuitka_CallMethodFunctionNoArgs(
                    tstate,
                    (struct Nuitka_FunctionObject const *)descr,
                    source
                );
{% endif %}
                Py_DECREF(descr);

                return result;
            } else {
                PyObject *called_object = func(descr, source, (PyObject *)type);
                CHECK_OBJECT(called_object);

                Py_DECREF(descr);

                PyObject *result = {{ call_function_with_args( "called_object", "args", args_count) }};
                Py_DECREF(called_object);
                return result;
            }
        }

        if (descr != NULL) {
            CHECK_OBJECT(descr);

            PyObject *result = {{ call_function_with_args( "descr", "args", args_count) }};
            Py_DECREF(descr);
            return result;
        }

#if PYTHON_VERSION < 0x300
        SET_CURRENT_EXCEPTION_TYPE0_FORMAT2(
            PyExc_AttributeError,
            "'%s' object has no attribute '%s'",
            type->tp_name,
            PyString_AS_STRING(attr_name)
        );
#else
        PyErr_Format(
            PyExc_AttributeError,
            "'%s' object has no attribute '%U'",
            type->tp_name,
            attr_name
        );
#endif
        return NULL;
    }
#if PYTHON_VERSION < 0x300
    else if (type == &PyInstance_Type) {
        PyInstanceObject *source_instance = (PyInstanceObject *)source;

        // The special cases have their own variant on the code generation level
        // as we are called with constants only.
        assert(attr_name != const_str_plain___dict__);
        assert(attr_name != const_str_plain___class__);

        // Try the instance dict first.
        PyObject *called_object = GET_STRING_DICT_VALUE(
            (PyDictObject *)source_instance->in_dict,
            (PyStringObject *)attr_name
        );

        // Note: The "called_object" was found without taking a reference,
        // so we need not release it in this branch.
        if (called_object != NULL) {
            return {{ call_function_with_args( "called_object", "args", args_count) }};
        }

        // Then check the class dictionaries.
        called_object = FIND_ATTRIBUTE_IN_CLASS(
            source_instance->in_class,
            attr_name
        );

        // Note: The "called_object" was found without taking a reference,
        // so we need not release it in this branch.
        if (called_object != NULL) {
            descrgetfunc descr_get = Py_TYPE(called_object)->tp_descr_get;

            if (descr_get == Nuitka_Function_Type.tp_descr_get) {
{% if args_count != 0 %}
                return Nuitka_CallMethodFunctionPosArgs(
                    tstate,
                    (struct Nuitka_FunctionObject const *)called_object,
                    source,
                    args,
                    {{ args_count }}
                );
{% else %}
                return Nuitka_CallMethodFunctionNoArgs(
                    tstate,
                    (struct Nuitka_FunctionObject const *)called_object,
                    source
                );
{% endif %}
            } else if (descr_get != NULL) {
                PyObject *method = descr_get(
                    called_object,
                    source,
                    (PyObject *)source_instance->in_class
                );

                if (unlikely(method == NULL)) {
                    return NULL;
                }

                PyObject *result = {{ call_function_with_args( "method", "args", args_count) }};
                Py_DECREF(method);
                return result;
            } else {
                return {{ call_function_with_args( "called_object", "args", args_count) }};
            }

        } else if (unlikely(source_instance->in_class->cl_getattr == NULL)) {
            SET_CURRENT_EXCEPTION_TYPE0_FORMAT2(
                PyExc_AttributeError,
                "%s instance has no attribute '%s'",
                PyString_AS_STRING(source_instance->in_class->cl_name),
                PyString_AS_STRING(attr_name)
            );

            return NULL;
        } else {
            // Finally allow the "__getattr__" override to provide it or else
            // it's an error.

            PyObject *args2[] = {
                source,
                attr_name
            };

            called_object = CALL_FUNCTION_WITH_ARGS2(
                tstate,
                source_instance->in_class->cl_getattr,
                args2
            );

            if (unlikely(called_object == NULL)) {
                return NULL;
            }

            PyObject *result = {{ call_function_with_args( "called_object", "args", args_count) }};
            Py_DECREF(called_object);
            return result;
        }
    }
#endif
    else if (type->tp_getattro != NULL) {
        PyObject *descr = (*type->tp_getattro)(source, attr_name);

        if (unlikely(descr == NULL)) {
            return NULL;
        }

        descrgetfunc func = NULL;
        if (NuitkaType_HasFeatureClass(Py_TYPE(descr))) {
            func = Py_TYPE(descr)->tp_descr_get;

            if (func != NULL && PyDescr_IsData(descr)) {
                PyObject *called_object = func(descr, source, (PyObject *)type);
                Py_DECREF(descr);

                if (unlikely(called_object == NULL))
                {
                    return NULL;
                }

                PyObject *result = {{ call_function_with_args("called_object", "args", args_count) }};
                Py_DECREF(called_object);
                return result;
            }
        }

        PyObject *result = {{ call_function_with_args("descr", "args", args_count) }};
        Py_DECREF(descr);
        return result;
    } else if (type->tp_getattr != NULL) {
        PyObject *called_object = (*type->tp_getattr)(
            source,
            (char *)Nuitka_String_AsString_Unchecked(attr_name)
        );

        if (unlikely(called_object == NULL)) {
            return NULL;
        }

        PyObject *result = {{ call_function_with_args( "called_object", "args", args_count) }};
        Py_DECREF(called_object);
        return result;
    } else {
        SET_CURRENT_EXCEPTION_TYPE0_FORMAT2(
            PyExc_AttributeError,
            "'%s' object has no attribute '%s'",
            type->tp_name,
            Nuitka_String_AsString_Unchecked(attr_name)
        );

        return NULL;
    }
}
