/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */
/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */

/* C-based Tracer for coverage.py. */

#include "util.h"
#include "datastack.h"
#include "filedisp.h"
#include "tracer.h"

/* Python C API helpers. */

static int
pyint_as_int(PyObject * pyint, int *pint)
{
    int the_int = (int)PyLong_AsLong(pyint);
    if (the_int == -1 && PyErr_Occurred()) {
        return RET_ERROR;
    }

    *pint = the_int;
    return RET_OK;
}


/* Interned strings to speed GetAttr etc. */

static PyObject *str__coverage_plugin;
static PyObject *str__coverage_plugin_name;
static PyObject *str_dynamic_source_filename;
static PyObject *str_line_number_range;

int
CTracer_intern_strings(void)
{
    int ret = RET_ERROR;

#define INTERN_STRING(v, s)                     \
    v = PyUnicode_InternFromString(s);          \
    if (v == NULL) {                            \
        goto error;                             \
    }

    INTERN_STRING(str__coverage_plugin, "_coverage_plugin")
    INTERN_STRING(str__coverage_plugin_name, "_coverage_plugin_name")
    INTERN_STRING(str_dynamic_source_filename, "dynamic_source_filename")
    INTERN_STRING(str_line_number_range, "line_number_range")

    ret = RET_OK;

error:
    return ret;
}

static void CTracer_disable_plugin(CTracer *self, PyObject * disposition);

static int
CTracer_init(CTracer *self, PyObject *args_unused, PyObject *kwds_unused)
{
    int ret = RET_ERROR;

    if (DataStack_init(&self->stats, &self->data_stack) < 0) {
        goto error;
    }

    self->pdata_stack = &self->data_stack;

    self->context = Py_None;
    Py_INCREF(self->context);

    ret = RET_OK;
    goto ok;

error:
    STATS( self->stats.errors++; )

ok:
    return ret;
}

static void
CTracer_dealloc(CTracer *self)
{
    int i;

    if (self->started) {
        PyEval_SetTrace(NULL, NULL);
    }

    Py_XDECREF(self->should_trace);
    Py_XDECREF(self->check_include);
    Py_XDECREF(self->warn);
    Py_XDECREF(self->concur_id_func);
    Py_XDECREF(self->data);
    Py_XDECREF(self->file_tracers);
    Py_XDECREF(self->should_trace_cache);
    Py_XDECREF(self->should_start_context);
    Py_XDECREF(self->switch_context);
    Py_XDECREF(self->lock_data);
    Py_XDECREF(self->unlock_data);
    Py_XDECREF(self->context);
    Py_XDECREF(self->disable_plugin);

    DataStack_dealloc(&self->stats, &self->data_stack);
    if (self->data_stacks) {
        for (i = 0; i < self->data_stacks_used; i++) {
            DataStack_dealloc(&self->stats, self->data_stacks + i);
        }
        PyMem_Free(self->data_stacks);
    }

    Py_XDECREF(self->data_stack_index);

    Py_TYPE(self)->tp_free((PyObject*)self);
}

#if TRACE_LOG
/* Set debugging constants: a file substring and line number to start logging. */
static const char * start_file = "badasync.py";
static int start_line = 1;

static const char *
indent(int n)
{
    static const char * spaces =
        "                                                                    "
        "                                                                    "
        "                                                                    "
        "                                                                    "
        ;
    return spaces + strlen(spaces) - n*2;
}

static BOOL logging = FALSE;

static void
CTracer_showlog(CTracer * self, int lineno, PyObject * filename, const char * msg)
{
    if (logging) {
        int depth = self->pdata_stack->depth;
        printf("%x: %s%3d ", (int)self, indent(depth), depth);
        if (lineno) {
            printf("%4d", lineno);
        }
        else {
            printf("    ");
        }
        if (filename) {
            PyObject *ascii = PyUnicode_AsASCIIString(filename);
            printf(" %s", PyBytes_AS_STRING(ascii));
            Py_DECREF(ascii);
        }
        if (msg) {
            printf(" %s", msg);
        }
        printf("\n");
    }
}

#define SHOWLOG(l,f,m)    CTracer_showlog(self,l,f,m)
#else
#define SHOWLOG(l,f,m)
#endif /* TRACE_LOG */

#if WHAT_LOG
static const char * what_sym[] = {"CALL", "EXC ", "LINE", "RET "};
#endif

/* Record a pair of integers in self->pcur_entry->file_data. */
static int
CTracer_record_pair(CTracer *self, int l1, int l2)
{
    int ret = RET_ERROR;
    PyObject * packed_obj = NULL;
    uint64 packed = 0;

    // Conceptually, data is a set of tuples (l1, l2), but that literally
    // making a set of tuples would require us to construct a tuple just to
    // see if we'd already recorded an arc.  On many-times-executed code,
    // that would mean we construct a tuple, find the tuple is already in the
    // set, then discard the tuple.  We can avoid that overhead by packing
    // the two line numbers into one integer instead.
    // See collector.py:flush_data for the Python code that unpacks this.
    if (l1 < 0) {
        packed |= (1LL << 40);
        l1 = -l1;
    }
    if (l2 < 0) {
        packed |= (1LL << 41);
        l2 = -l2;
    }
    packed |= (((uint64)l2) << 20) + (uint64)l1;
    packed_obj = PyLong_FromUnsignedLongLong(packed);
    if (packed_obj == NULL) {
        goto error;
    }

    if (PySet_Add(self->pcur_entry->file_data, packed_obj) < 0) {
        goto error;
    }

    ret = RET_OK;

error:
    Py_XDECREF(packed_obj);

    return ret;
}

/* Set self->pdata_stack to the proper data_stack to use. */
static int
CTracer_set_pdata_stack(CTracer *self)
{
    int ret = RET_ERROR;
    PyObject * co_obj = NULL;
    PyObject * stack_index = NULL;

    if (self->concur_id_func != Py_None) {
        int the_index = 0;

        if (self->data_stack_index == NULL) {
            PyObject * weakref = NULL;

            weakref = PyImport_ImportModule("weakref");
            if (weakref == NULL) {
                goto error;
            }
            STATS( self->stats.pycalls++; )
            self->data_stack_index = PyObject_CallMethod(weakref, "WeakKeyDictionary", NULL);
            Py_XDECREF(weakref);

            if (self->data_stack_index == NULL) {
                goto error;
            }
        }

        STATS( self->stats.pycalls++; )
        co_obj = PyObject_CallObject(self->concur_id_func, NULL);
        if (co_obj == NULL) {
            goto error;
        }
        stack_index = PyObject_GetItem(self->data_stack_index, co_obj);
        if (stack_index == NULL) {
            /* PyObject_GetItem sets an exception if it didn't find the thing. */
            PyErr_Clear();

            /* A new concurrency object.  Make a new data stack. */
            the_index = self->data_stacks_used;
            stack_index = PyLong_FromLong((long)the_index);
            if (stack_index == NULL) {
                goto error;
            }
            if (PyObject_SetItem(self->data_stack_index, co_obj, stack_index) < 0) {
                goto error;
            }
            self->data_stacks_used++;
            if (self->data_stacks_used >= self->data_stacks_alloc) {
                int bigger = self->data_stacks_alloc + 10;
                DataStack * bigger_stacks = PyMem_Realloc(self->data_stacks, bigger * sizeof(DataStack));
                if (bigger_stacks == NULL) {
                    PyErr_NoMemory();
                    goto error;
                }
                self->data_stacks = bigger_stacks;
                self->data_stacks_alloc = bigger;
            }
            DataStack_init(&self->stats, &self->data_stacks[the_index]);
        }
        else {
            if (pyint_as_int(stack_index, &the_index) < 0) {
                goto error;
            }
        }

        self->pdata_stack = &self->data_stacks[the_index];
    }
    else {
        self->pdata_stack = &self->data_stack;
    }

    ret = RET_OK;

error:

    Py_XDECREF(co_obj);
    Py_XDECREF(stack_index);

    return ret;
}

/*
 * Parts of the trace function.
 */

static int
CTracer_handle_call(CTracer *self, PyFrameObject *frame)
{
    int ret = RET_ERROR;
    int ret2;

    /* Owned references that we clean up at the very end of the function. */
    PyObject * disposition = NULL;
    PyObject * plugin = NULL;
    PyObject * plugin_name = NULL;
    PyObject * next_tracename = NULL;
#ifdef RESUME
    PyObject * pCode = NULL;
#endif

    /* Borrowed references. */
    PyObject * filename = NULL;
    PyObject * disp_trace = NULL;
    PyObject * tracename = NULL;
    PyObject * file_tracer = NULL;
    PyObject * has_dynamic_filename = NULL;

    CFileDisposition * pdisp = NULL;

    STATS( self->stats.calls++; )

    /* Grow the stack. */
    if (CTracer_set_pdata_stack(self) < 0) {
        goto error;
    }
    if (DataStack_grow(&self->stats, self->pdata_stack) < 0) {
        goto error;
    }
    self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth];

    /* See if this frame begins a new context. */
    if (self->should_start_context != Py_None && self->context == Py_None) {
        PyObject * context;
        /* We're looking for our context, ask should_start_context if this is the start. */
        STATS( self->stats.start_context_calls++; )
        STATS( self->stats.pycalls++; )
        context = PyObject_CallFunctionObjArgs(self->should_start_context, frame, NULL);
        if (context == NULL) {
            goto error;
        }
        if (context != Py_None) {
            PyObject * val;
            Py_DECREF(self->context);
            self->context = context;
            self->pcur_entry->started_context = TRUE;
            STATS( self->stats.pycalls++; )
            val = PyObject_CallFunctionObjArgs(self->switch_context, context, NULL);
            if (val == NULL) {
                goto error;
            }
            Py_DECREF(val);
        }
        else {
            Py_DECREF(context);
            self->pcur_entry->started_context = FALSE;
        }
    }
    else {
        self->pcur_entry->started_context = FALSE;
    }

    /* Check if we should trace this line. */
    filename = MyFrame_GetCode(frame)->co_filename;
    disposition = PyDict_GetItem(self->should_trace_cache, filename);
    if (disposition == NULL) {
        if (PyErr_Occurred()) {
            goto error;
        }
        STATS( self->stats.files++; )

        /* We've never considered this file before. */
        /* Ask should_trace about it. */
        STATS( self->stats.pycalls++; )
        disposition = PyObject_CallFunctionObjArgs(self->should_trace, filename, frame, NULL);
        if (disposition == NULL) {
            /* An error occurred inside should_trace. */
            goto error;
        }
        if (PyDict_SetItem(self->should_trace_cache, filename, disposition) < 0) {
            goto error;
        }
    }
    else {
        Py_INCREF(disposition);
    }

    if (disposition == Py_None) {
        /* A later check_include returned false, so don't trace it. */
        disp_trace = Py_False;
    }
    else {
        /* The object we got is a CFileDisposition, use it efficiently. */
        pdisp = (CFileDisposition *) disposition;
        disp_trace = pdisp->trace;
        if (disp_trace == NULL) {
            goto error;
        }
    }

    if (disp_trace == Py_True) {
        /* If tracename is a string, then we're supposed to trace. */
        tracename = pdisp->source_filename;
        if (tracename == NULL) {
            goto error;
        }
        file_tracer = pdisp->file_tracer;
        if (file_tracer == NULL) {
            goto error;
        }
        if (file_tracer != Py_None) {
            plugin = PyObject_GetAttr(file_tracer, str__coverage_plugin);
            if (plugin == NULL) {
                goto error;
            }
            plugin_name = PyObject_GetAttr(plugin, str__coverage_plugin_name);
            if (plugin_name == NULL) {
                goto error;
            }
        }
        has_dynamic_filename = pdisp->has_dynamic_filename;
        if (has_dynamic_filename == NULL) {
            goto error;
        }
        if (has_dynamic_filename == Py_True) {
            STATS( self->stats.pycalls++; )
            next_tracename = PyObject_CallMethodObjArgs(
                file_tracer, str_dynamic_source_filename,
                tracename, frame, NULL
                );
            if (next_tracename == NULL) {
                /* An exception from the function. Alert the user with a
                 * warning and a traceback.
                 */
                CTracer_disable_plugin(self, disposition);
                /* Because we handled the error, goto ok. */
                goto ok;
            }
            tracename = next_tracename;

            if (tracename != Py_None) {
                /* Check the dynamic source filename against the include rules. */
                PyObject * included = NULL;
                int should_include;
                included = PyDict_GetItem(self->should_trace_cache, tracename);
                if (included == NULL) {
                    PyObject * should_include_bool;
                    if (PyErr_Occurred()) {
                        goto error;
                    }
                    STATS( self->stats.files++; )
                    STATS( self->stats.pycalls++; )
                    should_include_bool = PyObject_CallFunctionObjArgs(self->check_include, tracename, frame, NULL);
                    if (should_include_bool == NULL) {
                        goto error;
                    }
                    should_include = (should_include_bool == Py_True);
                    Py_DECREF(should_include_bool);
                    if (PyDict_SetItem(self->should_trace_cache, tracename, should_include ? disposition : Py_None) < 0) {
                        goto error;
                    }
                }
                else {
                    should_include = (included != Py_None);
                }
                if (!should_include) {
                    tracename = Py_None;
                }
            }
        }
    }
    else {
        tracename = Py_None;
    }

    if (tracename != Py_None) {
        PyObject * file_data;
        BOOL had_error = FALSE;
        PyObject * res;

        res = PyObject_CallFunctionObjArgs(self->lock_data, NULL);
        if (res == NULL) {
            goto error;
        }

        file_data = PyDict_GetItem(self->data, tracename);

        if (file_data == NULL) {
            if (PyErr_Occurred()) {
                had_error = TRUE;
                goto unlock;
            }
            file_data = PySet_New(NULL);
            if (file_data == NULL) {
                had_error = TRUE;
                goto unlock;
            }
            ret2 = PyDict_SetItem(self->data, tracename, file_data);
            if (ret2 < 0) {
                had_error = TRUE;
                goto unlock;
            }

            /* If the disposition mentions a plugin, record that. */
            if (file_tracer != Py_None) {
                ret2 = PyDict_SetItem(self->file_tracers, tracename, plugin_name);
                if (ret2 < 0) {
                    had_error = TRUE;
                    goto unlock;
                }
            }
        }
        else {
            /* PyDict_GetItem gives a borrowed reference. Own it. */
            Py_INCREF(file_data);
        }

        unlock:

        res = PyObject_CallFunctionObjArgs(self->unlock_data, NULL);
        if (res == NULL) {
            goto error;
        }

        if (had_error) {
            goto error;
        }

        Py_XDECREF(self->pcur_entry->file_data);
        self->pcur_entry->file_data = file_data;
        self->pcur_entry->file_tracer = file_tracer;

        SHOWLOG(PyFrame_GetLineNumber(frame), filename, "traced");
    }
    else {
        Py_XDECREF(self->pcur_entry->file_data);
        self->pcur_entry->file_data = NULL;
        self->pcur_entry->file_tracer = Py_None;
        MyFrame_NoTraceLines(frame);
        SHOWLOG(PyFrame_GetLineNumber(frame), filename, "skipped");
    }

    self->pcur_entry->disposition = disposition;

    /* Make the frame right in case settrace(gettrace()) happens. */
    MyFrame_SetTrace(frame, self);

    /* A call event is really a "start frame" event, and can happen for
     * re-entering a generator also.  How we tell the difference depends on
     * the version of Python.
     */
    BOOL real_call = FALSE;

#ifdef RESUME
    /*
     * The current opcode is guaranteed to be RESUME. The argument
     * determines what kind of resume it is.
     */
    pCode = MyCode_GetCode(MyFrame_GetCode(frame));
    real_call = (PyBytes_AS_STRING(pCode)[MyFrame_GetLasti(frame) + 1] == 0);
#else
    // f_lasti is -1 for a true call, and a real byte offset for a generator re-entry.
    real_call = (MyFrame_GetLasti(frame) < 0);
#endif

    if (real_call) {
        self->pcur_entry->last_line = -MyFrame_GetCode(frame)->co_firstlineno;
    }
    else {
        self->pcur_entry->last_line = PyFrame_GetLineNumber(frame);
    }

ok:
    ret = RET_OK;

error:
#ifdef RESUME
    MyCode_FreeCode(pCode);
#endif
    Py_XDECREF(next_tracename);
    Py_XDECREF(disposition);
    Py_XDECREF(plugin);
    Py_XDECREF(plugin_name);

    return ret;
}


static void
CTracer_disable_plugin(CTracer *self, PyObject * disposition)
{
    PyObject * ret;
    PyErr_Print();

    STATS( self->stats.pycalls++; )
    ret = PyObject_CallFunctionObjArgs(self->disable_plugin, disposition, NULL);
    if (ret == NULL) {
        goto error;
    }
    Py_DECREF(ret);

    return;

error:
    /* This function doesn't return a status, so if an error happens, print it,
     * but don't interrupt the flow. */
    /* PySys_WriteStderr is nicer, but is not in the public API. */
    fprintf(stderr, "Error occurred while disabling plug-in:\n");
    PyErr_Print();
}


static int
CTracer_unpack_pair(CTracer *self, PyObject *pair, int *p_one, int *p_two)
{
    int ret = RET_ERROR;
    int the_int;
    PyObject * pyint = NULL;
    int index;

    if (!PyTuple_Check(pair) || PyTuple_Size(pair) != 2) {
        PyErr_SetString(
            PyExc_TypeError,
            "line_number_range must return 2-tuple"
            );
        goto error;
    }

    for (index = 0; index < 2; index++) {
        pyint = PyTuple_GetItem(pair, index);
        if (pyint == NULL) {
            goto error;
        }
        if (pyint_as_int(pyint, &the_int) < 0) {
            goto error;
        }
        *(index == 0 ? p_one : p_two) = the_int;
    }

    ret = RET_OK;

error:
    return ret;
}

static int
CTracer_handle_line(CTracer *self, PyFrameObject *frame)
{
    int ret = RET_ERROR;
    int ret2;

    STATS( self->stats.lines++; )
    if (self->pdata_stack->depth >= 0) {
        SHOWLOG(PyFrame_GetLineNumber(frame), MyFrame_GetCode(frame)->co_filename, "line");
        if (self->pcur_entry->file_data) {
            int lineno_from = -1;
            int lineno_to = -1;

            /* We're tracing in this frame: record something. */
            if (self->pcur_entry->file_tracer != Py_None) {
                PyObject * from_to = NULL;
                STATS( self->stats.pycalls++; )
                from_to = PyObject_CallMethodObjArgs(self->pcur_entry->file_tracer, str_line_number_range, frame, NULL);
                if (from_to == NULL) {
                    CTracer_disable_plugin(self, self->pcur_entry->disposition);
                    goto ok;
                }
                ret2 = CTracer_unpack_pair(self, from_to, &lineno_from, &lineno_to);
                Py_DECREF(from_to);
                if (ret2 < 0) {
                    CTracer_disable_plugin(self, self->pcur_entry->disposition);
                    goto ok;
                }
            }
            else {
                lineno_from = lineno_to = PyFrame_GetLineNumber(frame);
            }

            if (lineno_from != -1) {
                for (; lineno_from <= lineno_to; lineno_from++) {
                    if (self->tracing_arcs) {
                        /* Tracing arcs: key is (last_line,this_line). */
                        if (CTracer_record_pair(self, self->pcur_entry->last_line, lineno_from) < 0) {
                            goto error;
                        }
                    }
                    else {
                        /* Tracing lines: key is simply this_line. */
                        PyObject * this_line = PyLong_FromLong((long)lineno_from);
                        if (this_line == NULL) {
                            goto error;
                        }

                        ret2 = PySet_Add(self->pcur_entry->file_data, this_line);
                        Py_DECREF(this_line);
                        if (ret2 < 0) {
                            goto error;
                        }
                    }

                    self->pcur_entry->last_line = lineno_from;
                }
            }
        }
    }

ok:
    ret = RET_OK;

error:

    return ret;
}

static int
CTracer_handle_return(CTracer *self, PyFrameObject *frame)
{
    int ret = RET_ERROR;

    PyObject * pCode = NULL;

    STATS( self->stats.returns++; )
    /* A near-copy of this code is above in the missing-return handler. */
    if (CTracer_set_pdata_stack(self) < 0) {
        goto error;
    }
    self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth];

    if (self->pdata_stack->depth >= 0) {
        if (self->tracing_arcs && self->pcur_entry->file_data) {
            BOOL real_return = FALSE;
            pCode = MyCode_GetCode(MyFrame_GetCode(frame));
            int lasti = MyFrame_GetLasti(frame);
            Py_ssize_t code_size = PyBytes_GET_SIZE(pCode);
            unsigned char * code_bytes = (unsigned char *)PyBytes_AS_STRING(pCode);
#ifdef RESUME
            if (lasti == code_size - 2) {
                real_return = TRUE;
            }
            else {
#if ENV_LASTI_IS_YIELD
                lasti += 2;
#endif
                real_return = (code_bytes[lasti] != RESUME);
            }
#else
            /* Need to distinguish between RETURN_VALUE and YIELD_VALUE. Read
             * the current bytecode to see what it is.  In unusual circumstances
             * (Cython code), co_code can be the empty string, so range-check
             * f_lasti before reading the byte.
             */
            BOOL is_yield = FALSE;
            BOOL is_yield_from = FALSE;

            if (lasti < code_size) {
                is_yield = (code_bytes[lasti] == YIELD_VALUE);
                if (lasti + 2 < code_size) {
                    is_yield_from = (code_bytes[lasti + 2] == YIELD_FROM);
                }
            }
            real_return = !(is_yield || is_yield_from);
#endif
            if (real_return) {
                int first = MyFrame_GetCode(frame)->co_firstlineno;
                if (CTracer_record_pair(self, self->pcur_entry->last_line, -first) < 0) {
                    goto error;
                }
            }
        }

        /* If this frame started a context, then returning from it ends the context. */
        if (self->pcur_entry->started_context) {
            PyObject * val;
            Py_DECREF(self->context);
            self->context = Py_None;
            Py_INCREF(self->context);
            STATS( self->stats.pycalls++; )

            val = PyObject_CallFunctionObjArgs(self->switch_context, self->context, NULL);
            if (val == NULL) {
                goto error;
            }
            Py_DECREF(val);
        }

        /* Pop the stack. */
        SHOWLOG(PyFrame_GetLineNumber(frame), MyFrame_GetCode(frame)->co_filename, "return");
        self->pdata_stack->depth--;
        self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth];
    }

    ret = RET_OK;

error:

    MyCode_FreeCode(pCode);
    return ret;
}

/*
 * The Trace Function
 */
static int
CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unused)
{
    int ret = RET_ERROR;

    #if DO_NOTHING
    return RET_OK;
    #endif

    if (!self->started) {
        /* If CTracer.stop() has been called from another thread, the tracer
           is still active in the current thread. Let's deactivate ourselves
           now. */
        PyEval_SetTrace(NULL, NULL);
        return RET_OK;
    }

    #if WHAT_LOG || TRACE_LOG
    PyObject * ascii = NULL;
    #endif

    #if WHAT_LOG
    const char * w = "XXX ";
    if (what <= (int)(sizeof(what_sym)/sizeof(const char *))) {
        w = what_sym[what];
    }
    ascii = PyUnicode_AsASCIIString(MyFrame_GetCode(frame)->co_filename);
    printf("%x trace: f:%x %s @ %s %d\n", (int)self, (int)frame, what_sym[what], PyBytes_AS_STRING(ascii), PyFrame_GetLineNumber(frame));
    Py_DECREF(ascii);
    #endif

    #if TRACE_LOG
    ascii = PyUnicode_AsASCIIString(MyFrame_GetCode(frame)->co_filename);
    if (strstr(PyBytes_AS_STRING(ascii), start_file) && PyFrame_GetLineNumber(frame) == start_line) {
        logging = TRUE;
    }
    Py_DECREF(ascii);
    #endif

    self->activity = TRUE;

    switch (what) {
    case PyTrace_CALL:
        if (CTracer_handle_call(self, frame) < 0) {
            goto error;
        }
        break;

    case PyTrace_RETURN:
        if (CTracer_handle_return(self, frame) < 0) {
            goto error;
        }
        break;

    case PyTrace_LINE:
        if (CTracer_handle_line(self, frame) < 0) {
            goto error;
        }
        break;

    default:
        STATS( self->stats.others++; )
        break;
    }

    ret = RET_OK;
    goto cleanup;

error:
    STATS( self->stats.errors++; )

cleanup:
    return ret;
}


/*
 * Python has two ways to set the trace function: sys.settrace(fn), which
 * takes a Python callable, and PyEval_SetTrace(func, obj), which takes
 * a C function and a Python object.  The way these work together is that
 * sys.settrace(pyfn) calls PyEval_SetTrace(builtin_func, pyfn), using the
 * Python callable as the object in PyEval_SetTrace.  So sys.gettrace()
 * simply returns the Python object used as the second argument to
 * PyEval_SetTrace.  So sys.gettrace() will return our self parameter, which
 * means it must be callable to be used in sys.settrace().
 *
 * So we make ourself callable, equivalent to invoking our trace function.
 */
static PyObject *
CTracer_call(CTracer *self, PyObject *args, PyObject *kwds)
{
    PyFrameObject *frame;
    PyObject *what_str;
    PyObject *arg;
    int what;
    PyObject *ret = NULL;
    PyObject * ascii = NULL;

    #if DO_NOTHING
    CRASH
    #endif

    static char *what_names[] = {
        "call", "exception", "line", "return",
        "c_call", "c_exception", "c_return",
        NULL
        };

    static char *kwlist[] = {"frame", "event", "arg", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O!O|i:Tracer_call", kwlist,
            &PyFrame_Type, &frame, &PyUnicode_Type, &what_str, &arg)) {
        goto done;
    }

    /* In Python, the what argument is a string, we need to find an int
       for the C function. */
    for (what = 0; what_names[what]; what++) {
        int should_break;
        ascii = PyUnicode_AsASCIIString(what_str);
        should_break = !strcmp(PyBytes_AS_STRING(ascii), what_names[what]);
        Py_DECREF(ascii);
        if (should_break) {
            break;
        }
    }

    #if WHAT_LOG
    ascii = PyUnicode_AsASCIIString(MyFrame_GetCode(frame)->co_filename);
    printf("pytrace: %s @ %s %d\n", what_sym[what], PyBytes_AS_STRING(ascii), PyFrame_GetLineNumber(frame));
    Py_DECREF(ascii);
    #endif

    /* Invoke the C function, and return ourselves. */
    if (CTracer_trace(self, frame, what, arg) == RET_OK) {
        Py_INCREF(self);
        ret = (PyObject *)self;
    }

    /* For better speed, install ourselves the C way so that future calls go
       directly to CTracer_trace, without this intermediate function.

       Only do this if this is a CALL event, since new trace functions only
       take effect then.  If we don't condition it on CALL, then we'll clobber
       the new trace function before it has a chance to get called.  To
       understand why, there are three internal values to track: frame.f_trace,
       c_tracefunc, and c_traceobj.  They are explained here:
       https://nedbatchelder.com/text/trace-function.html

       Without the conditional on PyTrace_CALL, this is what happens:

            def func():                 #   f_trace         c_tracefunc     c_traceobj
                                        #   --------------  --------------  --------------
                                        #   CTracer         CTracer.trace   CTracer
                sys.settrace(my_func)
                                        #   CTracer         trampoline      my_func
                        # Now Python calls trampoline(CTracer), which calls this function
                        # which calls PyEval_SetTrace below, setting us as the tracer again:
                                        #   CTracer         CTracer.trace   CTracer
                        # and it's as if the settrace never happened.
        */
    if (what == PyTrace_CALL) {
        PyEval_SetTrace((Py_tracefunc)CTracer_trace, (PyObject*)self);
    }

done:
    return ret;
}

static PyObject *
CTracer_start(CTracer *self, PyObject *args_unused)
{
    PyEval_SetTrace((Py_tracefunc)CTracer_trace, (PyObject*)self);
    self->started = TRUE;
    self->tracing_arcs = self->trace_arcs && PyObject_IsTrue(self->trace_arcs);

    /* start() returns a trace function usable with sys.settrace() */
    Py_INCREF(self);
    return (PyObject *)self;
}

static PyObject *
CTracer_stop(CTracer *self, PyObject *args_unused)
{
    if (self->started) {
        /* Set the started flag only. The actual call to
           PyEval_SetTrace(NULL, NULL) is delegated to the callback
           itself to ensure that it called from the right thread.
           */
        self->started = FALSE;
    }

    Py_RETURN_NONE;
}

static PyObject *
CTracer_activity(CTracer *self, PyObject *args_unused)
{
    if (self->activity) {
        Py_RETURN_TRUE;
    }
    else {
        Py_RETURN_FALSE;
    }
}

static PyObject *
CTracer_reset_activity(CTracer *self, PyObject *args_unused)
{
    self->activity = FALSE;
    Py_RETURN_NONE;
}

static PyObject *
CTracer_get_stats(CTracer *self, PyObject *args_unused)
{
#if COLLECT_STATS
    return Py_BuildValue(
        "{sI,sI,sI,sI,sI,sI,si,sI,sI,sI}",
        "calls", self->stats.calls,
        "lines", self->stats.lines,
        "returns", self->stats.returns,
        "others", self->stats.others,
        "files", self->stats.files,
        "stack_reallocs", self->stats.stack_reallocs,
        "stack_alloc", self->pdata_stack->alloc,
        "errors", self->stats.errors,
        "pycalls", self->stats.pycalls,
        "start_context_calls", self->stats.start_context_calls
        );
#else
    Py_RETURN_NONE;
#endif /* COLLECT_STATS */
}

static PyMemberDef
CTracer_members[] = {
    { "should_trace",       T_OBJECT, offsetof(CTracer, should_trace), 0,
            PyDoc_STR("Function indicating whether to trace a file.") },

    { "check_include",      T_OBJECT, offsetof(CTracer, check_include), 0,
            PyDoc_STR("Function indicating whether to include a file.") },

    { "warn",               T_OBJECT, offsetof(CTracer, warn), 0,
            PyDoc_STR("Function for issuing warnings.") },

    { "concur_id_func",     T_OBJECT, offsetof(CTracer, concur_id_func), 0,
            PyDoc_STR("Function for determining concurrency context") },

    { "data",               T_OBJECT, offsetof(CTracer, data), 0,
            PyDoc_STR("The raw dictionary of trace data.") },

    { "file_tracers",       T_OBJECT, offsetof(CTracer, file_tracers), 0,
            PyDoc_STR("Mapping from file name to plugin name.") },

    { "should_trace_cache", T_OBJECT, offsetof(CTracer, should_trace_cache), 0,
            PyDoc_STR("Dictionary caching should_trace results.") },

    { "trace_arcs",         T_OBJECT, offsetof(CTracer, trace_arcs), 0,
            PyDoc_STR("Should we trace arcs, or just lines?") },

    { "should_start_context", T_OBJECT, offsetof(CTracer, should_start_context), 0,
            PyDoc_STR("Function for starting contexts.") },

    { "switch_context",     T_OBJECT, offsetof(CTracer, switch_context), 0,
            PyDoc_STR("Function for switching to a new context.") },

    { "lock_data",          T_OBJECT, offsetof(CTracer, lock_data), 0,
            PyDoc_STR("Function for locking access to self.data.") },

    { "unlock_data",        T_OBJECT, offsetof(CTracer, unlock_data), 0,
            PyDoc_STR("Function for unlocking access to self.data.") },

    { "disable_plugin",     T_OBJECT, offsetof(CTracer, disable_plugin), 0,
            PyDoc_STR("Function for disabling a plugin.") },

    { NULL }
};

static PyMethodDef
CTracer_methods[] = {
    { "start",      (PyCFunction) CTracer_start,        METH_VARARGS,
            PyDoc_STR("Start the tracer") },

    { "stop",       (PyCFunction) CTracer_stop,         METH_VARARGS,
            PyDoc_STR("Stop the tracer") },

    { "get_stats",  (PyCFunction) CTracer_get_stats,    METH_VARARGS,
            PyDoc_STR("Get statistics about the tracing") },

    { "activity",   (PyCFunction) CTracer_activity,     METH_VARARGS,
            PyDoc_STR("Has there been any activity?") },

    { "reset_activity", (PyCFunction) CTracer_reset_activity, METH_VARARGS,
            PyDoc_STR("Reset the activity flag") },

    { NULL }
};

PyTypeObject
CTracerType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "coverage.CTracer",        /*tp_name*/
    sizeof(CTracer),           /*tp_basicsize*/
    0,                         /*tp_itemsize*/
    (destructor)CTracer_dealloc, /*tp_dealloc*/
    0,                         /*tp_print*/
    0,                         /*tp_getattr*/
    0,                         /*tp_setattr*/
    0,                         /*tp_compare*/
    0,                         /*tp_repr*/
    0,                         /*tp_as_number*/
    0,                         /*tp_as_sequence*/
    0,                         /*tp_as_mapping*/
    0,                         /*tp_hash */
    (ternaryfunc)CTracer_call, /*tp_call*/
    0,                         /*tp_str*/
    0,                         /*tp_getattro*/
    0,                         /*tp_setattro*/
    0,                         /*tp_as_buffer*/
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
    "CTracer objects",         /* tp_doc */
    0,                         /* tp_traverse */
    0,                         /* tp_clear */
    0,                         /* tp_richcompare */
    0,                         /* tp_weaklistoffset */
    0,                         /* tp_iter */
    0,                         /* tp_iternext */
    CTracer_methods,           /* tp_methods */
    CTracer_members,           /* tp_members */
    0,                         /* tp_getset */
    0,                         /* tp_base */
    0,                         /* tp_dict */
    0,                         /* tp_descr_get */
    0,                         /* tp_descr_set */
    0,                         /* tp_dictoffset */
    (initproc)CTracer_init,    /* tp_init */
    0,                         /* tp_alloc */
    0,                         /* tp_new */
};
