//     Copyright 2016, 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.
//

#include "nuitka/prelude.h"

#include "nuitka/compiled_method.h"

#include "structmember.h"

static PyObject *Nuitka_Method_get__doc__( struct Nuitka_MethodObject *method, void *closure )
{
    return INCREASE_REFCOUNT( method->m_function->m_doc );
}

static PyGetSetDef Nuitka_Method_getsets[] =
{
    { (char *)"__doc__", (getter)Nuitka_Method_get__doc__, NULL, NULL },
    { NULL }
};

#define OFF( x ) offsetof( struct Nuitka_MethodObject, x )

static PyMemberDef Nuitka_Method_members[] =
{
    { (char *)"im_class", T_OBJECT, OFF( m_class ), READONLY | RESTRICTED, (char *)"the class associated with a method"},
    { (char *)"im_func", T_OBJECT, OFF( m_function ), READONLY | RESTRICTED, (char *)"the function (or other callable) implementing a method" },
    { (char *)"__func__", T_OBJECT, OFF( m_function ), READONLY | RESTRICTED, (char *)"the function (or other callable) implementing a method" },
    { (char *)"im_self",  T_OBJECT, OFF( m_object ), READONLY | RESTRICTED, (char *)"the instance to which a method is bound; None for unbound method" },
    { (char *)"__self__", T_OBJECT, OFF( m_object ), READONLY | RESTRICTED, (char *)"the instance to which a method is bound; None for unbound method" },
    { NULL }
};

static PyObject *Nuitka_Method_reduce( struct Nuitka_MethodObject *method )
{
    PyErr_Format(
        PyExc_TypeError,
        "Can't pickle instancemethod objects"
    );

    return NULL;
}

static PyObject *Nuitka_Method_reduce_ex( struct Nuitka_MethodObject *method, PyObject *args )
{
    int proto;

    if ( !PyArg_ParseTuple(args, "|i:__reduce_ex__", &proto) )
    {
        return NULL;
    }

    PyErr_Format(
        PyExc_TypeError,
        "Can't pickle instancemethod objects"
    );

    return NULL;
}

static PyObject *Nuitka_Method_deepcopy( struct Nuitka_MethodObject *method, PyObject *memo )
{
    assert( Nuitka_Method_Check( (PyObject *)method ));

    static PyObject *module_copy = NULL;
    static PyObject *deepcopy_function = NULL;

    if ( module_copy == NULL  )
    {
        module_copy = PyImport_ImportModule( "copy" );
        CHECK_OBJECT( module_copy );

        deepcopy_function = PyObject_GetAttrString( module_copy, "deepcopy" );
        CHECK_OBJECT( deepcopy_function );
    }

    PyObject *object = PyObject_CallFunctionObjArgs( deepcopy_function, method->m_object, memo, NULL );

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

    return Nuitka_Method_New( method->m_function, object, method->m_class );
}



static PyMethodDef Nuitka_Method_methods[] =
{
    { "__reduce__",    (PyCFunction)Nuitka_Method_reduce,    METH_NOARGS,  NULL },
    { "__reduce_ex__", (PyCFunction)Nuitka_Method_reduce_ex, METH_VARARGS, NULL },
    { "__deepcopy__",  (PyCFunction)Nuitka_Method_deepcopy,  METH_O,       NULL },
    { NULL }
};

extern PyObject *const_str_plain___name__;

static char const *GET_CLASS_NAME( PyObject *klass )
{
    if ( klass == NULL )
    {
        return "?";
    }
    else
    {
#if PYTHON_VERSION < 300
        if ( PyClass_Check( klass ) )
        {
            return Nuitka_String_AsString( ((PyClassObject *)klass)->cl_name );
        }
#endif

        if ( !PyType_Check( klass ) )
        {
            klass = (PyObject *)Py_TYPE( klass );
        }

        return ((PyTypeObject *)klass)->tp_name;
    }
}

static char const *GET_INSTANCE_CLASS_NAME( PyObject *instance )
{
    // TODO: We have a constant for that already.
    PyObject *klass = PyObject_GetAttrString( instance, "__class__" );

    // Fallback to type as this cannot fail.
    if ( klass == NULL )
    {
        CLEAR_ERROR_OCCURRED();
        klass = INCREASE_REFCOUNT( (PyObject *)Py_TYPE( instance ) );
    }

    char const *result = GET_CLASS_NAME( klass );

    Py_DECREF( klass );

    return result;
}

static char const *GET_CALLABLE_DESC( PyObject *object )
{
    if ( Nuitka_Function_Check( object ) || Nuitka_Generator_Check( object ) || PyMethod_Check( object ) || PyFunction_Check( object ) || PyCFunction_Check( object ) )
    {
        return "()";
    }
#if PYTHON_VERSION < 300
    else if ( PyClass_Check( object ) )
    {
        return " constructor";
    }
    else if ( PyInstance_Check( object ))
    {
        return " instance";
    }
#endif
    else
    {
        return " object";
    }
}

static char const *GET_CALLABLE_NAME( PyObject *object )
{
    if ( Nuitka_Function_Check( object ) )
    {
        return Nuitka_String_AsString( Nuitka_Function_GetName( object ) );
    }
    else if ( Nuitka_Generator_Check( object ) )
    {
        return Nuitka_String_AsString( Nuitka_Generator_GetName( object ) );
    }
    else if ( PyMethod_Check( object ) )
    {
        return PyEval_GetFuncName( PyMethod_GET_FUNCTION( object ) );
    }
    else if ( PyFunction_Check( object ) )
    {
        return Nuitka_String_AsString( ((PyFunctionObject*)object)->func_name );
    }
#if PYTHON_VERSION < 300
    else if ( PyInstance_Check( object ) )
    {
        return Nuitka_String_AsString( ((PyInstanceObject*)object)->in_class->cl_name );
    }
    else if ( PyClass_Check( object ) )
    {
        return Nuitka_String_AsString( ((PyClassObject*)object)->cl_name );
    }
#endif
    else if ( PyCFunction_Check( object ) )
    {
        return ((PyCFunctionObject*)object)->m_ml->ml_name;
    }
    else
    {
        return Py_TYPE( object )->tp_name;
    }
}

static PyObject *Nuitka_Method_tp_call( struct Nuitka_MethodObject *method, PyObject *args, PyObject *kw )
{
    Py_ssize_t arg_count = PyTuple_Size( args );

    if ( method->m_object == NULL )
    {
        if (unlikely( arg_count < 1 ))
        {
            PyErr_Format(
                PyExc_TypeError,
                "unbound compiled_method %s%s must be called with %s instance as first argument (got nothing instead)",
                GET_CALLABLE_NAME( (PyObject *)method->m_function ),
                GET_CALLABLE_DESC( (PyObject *)method->m_function ),
                GET_CLASS_NAME( method->m_class )
            );
            return NULL;
        }
        else
        {
            PyObject *self = PyTuple_GET_ITEM( args, 0 );
            CHECK_OBJECT( self );

            int result = PyObject_IsInstance( self, method->m_class );

            if (unlikely( result < 0 ))
            {
                return NULL;
            }
            else if (unlikely( result == 0 ))
            {
                PyErr_Format(
                    PyExc_TypeError,
                    "unbound compiled_method %s%s must be called with %s instance as first argument (got %s instance instead)",
                    GET_CALLABLE_NAME( (PyObject *)method->m_function ),
                    GET_CALLABLE_DESC( (PyObject *)method->m_function ),
                    GET_CLASS_NAME( method->m_class ),
                    GET_INSTANCE_CLASS_NAME( (PyObject *)self )
                );

                return NULL;
            }
        }

        return Py_TYPE( method->m_function )->tp_call(
            (PyObject *)method->m_function, args, kw
        );
    }
    else
    {
        return Nuitka_CallMethodFunctionPosArgsKwArgs(
            method->m_function,
            method->m_object,
            &PyTuple_GET_ITEM( args, 0 ),
            arg_count,
            kw
        );
    }
}


static PyObject *Nuitka_Method_tp_descr_get( struct Nuitka_MethodObject *method, PyObject *object, PyObject *klass )
{
    // Don't rebind already bound methods.
    if ( method->m_object != NULL )
    {
        return INCREASE_REFCOUNT( (PyObject *)method );
    }

    if ( method->m_class != NULL && klass != NULL )
    {
        // Quick subclass test, bound methods remain the same if the class is a sub class
        int result = PyObject_IsSubclass( klass, method->m_class );

        if (unlikely( result < 0 ))
        {
            return NULL;
        }
        else if ( result == 0 )
        {
            return INCREASE_REFCOUNT( (PyObject *)method );
        }
    }

    return Nuitka_Method_New( method->m_function, object, klass );
}

static PyObject *Nuitka_Method_tp_getattro( struct Nuitka_MethodObject *method, PyObject *name )
{
    PyObject *descr = _PyType_Lookup( &Nuitka_Method_Type, name );

    if ( descr != NULL )
    {
        if (
#if PYTHON_VERSION < 300
            PyType_HasFeature( Py_TYPE( descr ), Py_TPFLAGS_HAVE_CLASS ) &&
#endif
            ( Py_TYPE( descr )->tp_descr_get != NULL )
           )
        {
            return Py_TYPE( descr )->tp_descr_get(
                descr,
                (PyObject *)method,
                (PyObject *)Py_TYPE( method )
            );
        }
        else
        {
            return INCREASE_REFCOUNT( descr );
        }
    }

    return PyObject_GetAttr( (PyObject *)method->m_function, name );
}


static long Nuitka_Method_tp_traverse( struct Nuitka_MethodObject *method, visitproc visit, void *arg )
{
    Py_VISIT( method->m_function );
    Py_VISIT( method->m_object );
    Py_VISIT( method->m_class );

    return 0;
}

 // tp_repr slot, decide how a function shall be output
static PyObject *Nuitka_Method_tp_repr( struct Nuitka_MethodObject *method )
{
    if ( method->m_object == NULL )
    {
#if PYTHON_VERSION < 300
        return PyString_FromFormat(
            "<unbound compiled_method %s.%s>",
            GET_CLASS_NAME( method->m_class ),
            Nuitka_String_AsString( method->m_function->m_name )
        );
#else
        return PyUnicode_FromFormat(
            "<compiled_function %s at %p>",
            Nuitka_String_AsString( method->m_function->m_name ),
            method->m_function
        );
#endif
    }
    else
    {
        // Note: CPython uses repr ob the object, although a comment despises
        // it, we do it for compatibility.
        PyObject *object_repr = PyObject_Repr( method->m_object );

        if ( object_repr == NULL )
        {
            return NULL;
        }
#if PYTHON_VERSION < 300
        else if ( !PyString_Check( object_repr ) )
        {
            Py_DECREF( object_repr );
            return NULL;
        }
#else
        else if ( !PyUnicode_Check( object_repr ) )
        {
            Py_DECREF( object_repr );
            return NULL;
        }
#endif

#if PYTHON_VERSION < 300
        PyObject *result = PyString_FromFormat(
            "<bound compiled_method %s.%s of %s>",
            GET_CLASS_NAME( method->m_class ),
            Nuitka_String_AsString( method->m_function->m_name ),
            Nuitka_String_AsString_Unchecked( object_repr )
        );
#elif PYTHON_VERSION < 350
        PyObject *result = PyUnicode_FromFormat(
            "<bound compiled_method %s.%s of %s>",
            GET_CLASS_NAME( method->m_class ),
            Nuitka_String_AsString( method->m_function->m_name ),
            Nuitka_String_AsString_Unchecked( object_repr )
        );
#else
        PyObject *result = PyUnicode_FromFormat(
            "<bound compiled_method %s of %s>",
            Nuitka_String_AsString( method->m_function->m_qualname ),
            Nuitka_String_AsString_Unchecked( object_repr )
        );
#endif

        Py_DECREF( object_repr );

        return result;
    }
}

#if PYTHON_VERSION < 300
static int Nuitka_Method_tp_compare( struct Nuitka_MethodObject *a, struct Nuitka_MethodObject *b )
{
    if ( a->m_function->m_counter < b->m_function->m_counter )
    {
        return -1;
    }
    else if ( a->m_function->m_counter > b->m_function->m_counter )
    {
        return 1;
    }
    else if ( a->m_object == b->m_object )
    {
        return 0;
    }
    else if ( a->m_object == NULL )
    {
        return -1;
    }
    else if ( b->m_object == NULL )
    {
        return 1;
    }
    else
    {
        return PyObject_Compare( a->m_object, b->m_object );
    }
}
#endif

static PyObject *Nuitka_Method_tp_richcompare( struct Nuitka_MethodObject *a, struct Nuitka_MethodObject *b, int op )
{
    if ( op != Py_EQ && op != Py_NE )
    {
        return INCREASE_REFCOUNT( Py_NotImplemented );
    }


    if ( Nuitka_Method_Check( (PyObject *)a ) == false || Nuitka_Method_Check( (PyObject *)b ) == false )
    {
        return INCREASE_REFCOUNT( Py_NotImplemented );
    }

    bool result = a->m_function->m_counter == b->m_function->m_counter;

    // If the underlying function objects are the same, check the objects, which
    // may be NULL in case of unbound methods, which would be the same again.
    if ( result )
    {
        if ( a->m_object == NULL )
        {
            result = b->m_object == NULL;
        }
        else if ( b->m_object == NULL )
        {
            result = 0;
        }
        else
        {
            int res = PyObject_RichCompareBool(
                a->m_object,
                b->m_object,
                Py_EQ
            );

            result = res != 0;
        }
    }

    if ( op == Py_EQ )
    {
        return INCREASE_REFCOUNT( BOOL_FROM( result ) );
    }
    else
    {
        return INCREASE_REFCOUNT( BOOL_FROM( !result ) );
    }
}


static long Nuitka_Method_tp_hash( struct Nuitka_MethodObject *method )
{
    // Just give the hash of the method function, that ought to be good enough.
    return method->m_function->m_counter;
}

// Cache for method object, try to avoid malloc overhead.
static struct Nuitka_MethodObject *method_cache_head = NULL;
static int method_cache_size = 0;
static const int max_method_cache_size = 4096;

static void Nuitka_Method_tp_dealloc( struct Nuitka_MethodObject *method )
{
#ifndef __NUITKA_NO_ASSERT__
    // Save the current exception, if any, we must to not corrupt it.
    PyObject *save_exception_type, *save_exception_value;
    PyTracebackObject *save_exception_tb;
    FETCH_ERROR_OCCURRED( &save_exception_type, &save_exception_value, &save_exception_tb );
    RESTORE_ERROR_OCCURRED( save_exception_type, save_exception_value, save_exception_tb );
#endif

    Nuitka_GC_UnTrack( method );

    if ( method->m_weakrefs != NULL )
    {
        PyObject_ClearWeakRefs( (PyObject *)method );
    }

    Py_XDECREF( method->m_object );
    Py_XDECREF( method->m_class );

    Py_DECREF( (PyObject *)method->m_function );

    if (likely( method_cache_size < max_method_cache_size ))
    {
        method->m_object = (PyObject *)method_cache_head;
        method_cache_head = method;
        method_cache_size += 1;
    }
    else {
        PyObject_GC_Del( method );
    }

#ifndef __NUITKA_NO_ASSERT__
    PyThreadState *tstate = PyThreadState_GET();

    assert( tstate->curexc_type == save_exception_type );
    assert( tstate->curexc_value == save_exception_value );
    assert( (PyTracebackObject *)tstate->curexc_traceback == save_exception_tb );
#endif
}

static PyObject *Nuitka_Method_tp_new( PyTypeObject* type, PyObject* args, PyObject *kw )
{
    PyObject *func;
    PyObject *self;
    PyObject *klass = NULL;

    if ( !_PyArg_NoKeywords( "instancemethod", kw ) )
    {
        return NULL;
    }
    else if ( !PyArg_UnpackTuple( args, "compiled_method", 2, 3, &func, &self, &klass ) )
    {
        return NULL;
    }
    else if ( !PyCallable_Check( func ) )
    {
        PyErr_Format( PyExc_TypeError, "first argument must be callable" );
        return NULL;
    }
    else
    {
        if ( self == Py_None )
        {
            self = NULL;
        }

        if ( self == NULL && klass == NULL )
        {
            PyErr_Format( PyExc_TypeError, "unbound methods must have non-NULL im_class" );
            return NULL;
        }
    }

    assert( Nuitka_Function_Check( func ) );

    return Nuitka_Method_New( (struct Nuitka_FunctionObject *)func, self, klass );
}

PyTypeObject Nuitka_Method_Type =
{
    PyVarObject_HEAD_INIT(NULL, 0)
    "compiled_method",
    sizeof(struct Nuitka_MethodObject),
    0,
    (destructor)Nuitka_Method_tp_dealloc,        // tp_dealloc
    0,                                           /* tp_print */
    0,                                           /* tp_getattr */
    0,                                           /* tp_setattr */
#if PYTHON_VERSION < 300
    (cmpfunc)Nuitka_Method_tp_compare,           /* tp_compare */
#else
    0,
#endif
    (reprfunc)Nuitka_Method_tp_repr,             /* tp_repr */
    0,                                           /* tp_as_number */
    0,                                           /* tp_as_sequence */
    0,                                           /* tp_as_mapping */
    (hashfunc)Nuitka_Method_tp_hash,             /* tp_hash */
    (ternaryfunc)Nuitka_Method_tp_call,          /* tp_call */
    0,                                           /* tp_str */
    (getattrofunc)Nuitka_Method_tp_getattro,     /* tp_getattro */
    PyObject_GenericSetAttr,                     /* tp_setattro */
    0,                                           /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT       |
#if PYTHON_VERSION < 300
    Py_TPFLAGS_HAVE_WEAKREFS |
#endif
    Py_TPFLAGS_HAVE_GC,                          /* tp_flags */
    0,                                           /* tp_doc */
    (traverseproc)Nuitka_Method_tp_traverse,     /* tp_traverse */
    0,                                           /* tp_clear */
    (richcmpfunc)Nuitka_Method_tp_richcompare,   /* tp_richcompare */
    offsetof(struct Nuitka_MethodObject, m_weakrefs), /* tp_weaklistoffset */
    0,                                           /* tp_iter */
    0,                                           /* tp_iternext */
    Nuitka_Method_methods,                       /* tp_methods */
    Nuitka_Method_members,                       /* tp_members */
    Nuitka_Method_getsets,                       /* tp_getset */
    0,                                           /* tp_base */
    0,                                           /* tp_dict */
    (descrgetfunc)Nuitka_Method_tp_descr_get,    /* tp_descr_get */
    0,                                           /* tp_descr_set */
    0,                                           /* tp_dictoffset */
    0,                                           /* tp_init */
    0,                                           /* tp_alloc */
    Nuitka_Method_tp_new,                        /* tp_new */
    0,                                           /* tp_free */
    0,                                           /* tp_is_gc */
    0,                                           /* tp_bases */
    0,                                           /* tp_mro */
    0,                                           /* tp_cache */
    0,                                           /* tp_subclasses */
    0,                                           /* tp_weaklist */
    0,                                           /* tp_del */
    0                                            /* tp_version_tag */
#if PYTHON_VERSION >= 340
    ,0                                           /* tp_finalizer */
#endif
};

void _initCompiledMethodType( void )
{
    PyType_Ready( &Nuitka_Method_Type );
}


PyObject *Nuitka_Method_New( struct Nuitka_FunctionObject *function, PyObject *object, PyObject *klass )
{
    struct Nuitka_MethodObject *result = method_cache_head;

    if ( result != NULL )
    {
        method_cache_head = (struct Nuitka_MethodObject *)method_cache_head->m_object;
        method_cache_size -= 1;

        Py_TYPE( result ) = &Nuitka_Method_Type;
        _Py_NewReference( (PyObject *)result );
    }
    else
    {
        result = PyObject_GC_New(struct Nuitka_MethodObject, &Nuitka_Method_Type);
    }

    if (unlikely( result == NULL ))
    {
        PyErr_Format(
            PyExc_RuntimeError,
            "cannot create method %s",
            Nuitka_String_AsString( function->m_name )
        );

        return NULL;
    }

    result->m_function = (struct Nuitka_FunctionObject *)INCREASE_REFCOUNT( (PyObject *)function );

    result->m_object = object;
    Py_XINCREF( object );
    result->m_class = klass;
    Py_XINCREF( klass );

    result->m_weakrefs = NULL;

    Nuitka_GC_Track( result );
    return (PyObject *)result;
}
