/**
 * PANDA 3D SOFTWARE
 * Copyright (c) Carnegie Mellon University.  All rights reserved.
 *
 * All use of this software is subject to the terms of the revised BSD
 * license.  You should have received a copy of this license along
 * with this source code in a file named "LICENSE."
 *
 * @file pythonTask.cxx
 * @author drose
 * @date 2008-09-16
 */

#include "pythonTask.h"
#include "pnotify.h"
#include "config_event.h"

#ifdef HAVE_PYTHON
#include "py_panda.h"

#include "pythonThread.h"
#include "asyncTaskManager.h"
#include "asyncFuture_ext.h"

TypeHandle PythonTask::_type_handle;

#ifndef CPPPARSER
extern struct Dtool_PyTypedObject Dtool_TypedReferenceCount;
extern struct Dtool_PyTypedObject Dtool_AsyncFuture;
extern struct Dtool_PyTypedObject Dtool_PythonTask;
#endif

/**
 *
 */
PythonTask::
PythonTask(PyObject *func_or_coro, const std::string &name) :
  AsyncTask(name),
  _function(nullptr),
  _args(nullptr),
  _upon_death(nullptr),
  _owner(nullptr),
  _exception(nullptr),
  _exc_value(nullptr),
  _exc_traceback(nullptr),
  _generator(nullptr),
  _fut_waiter(nullptr),
  _ignore_return(false),
  _registered_to_owner(false),
  _retrieved_exception(false) {

  nassertv(func_or_coro != nullptr);
  if (func_or_coro == Py_None || PyCallable_Check(func_or_coro)) {
    _function = Py_NewRef(func_or_coro);
  }
  else if (PyCoro_CheckExact(func_or_coro)) {
    // We also allow passing in a coroutine, because why not.
    _generator = Py_NewRef(func_or_coro);
  }
  else if (PyGen_CheckExact(func_or_coro)) {
    // Something emulating a coroutine.
    _generator = Py_NewRef(func_or_coro);
  }
  else {
    nassert_raise("Invalid function passed to PythonTask");
  }

  set_args(Py_None, true);
  set_upon_death(Py_None);
  set_owner(Py_None);

  __dict__ = PyDict_New();

#if !defined(SIMPLE_THREADS) && defined(WITH_THREAD) && PY_VERSION_HEX < 0x03090000
  // Ensure that the Python threading system is initialized and ready to go.
  // WITH_THREAD symbol defined within Python.h
  // PyEval_InitThreads is now a deprecated no-op in Python 3.9+
  PyEval_InitThreads();
#endif
}

/**
 *
 */
PythonTask::
~PythonTask() {
  // If the coroutine threw an exception, and there was no opportunity to
  // handle it, let the user know.
  if (_exception != nullptr && !_retrieved_exception) {
    task_cat.error()
      << *this << " exception was never retrieved:\n";
    PyErr_Restore(_exception, _exc_value, _exc_traceback);
    PyErr_Print();
    PyErr_Restore(nullptr, nullptr, nullptr);
    _exception = nullptr;
    _exc_value = nullptr;
    _exc_traceback = nullptr;
  }

  PyObject *self = __self__;
  if (self != nullptr) {
    PyObject_GC_UnTrack(self);
    __self__ = nullptr;
    Py_DECREF(self);
  }

  // All of these may have already been cleared by __clear__.
  Py_XDECREF(_function);
  Py_XDECREF(_args);
  Py_XDECREF(__dict__);
  Py_XDECREF(_exception);
  Py_XDECREF(_exc_value);
  Py_XDECREF(_exc_traceback);
  Py_XDECREF(_generator);
  Py_XDECREF(_owner);
  Py_XDECREF(_upon_death);
}

/**
 * Replaces the function that is called when the task runs.  The parameter
 * should be a Python callable object.
 */
void PythonTask::
set_function(PyObject *function) {
  Py_XSETREF(_function, Py_NewRef(function));

  if (_function != Py_None && !PyCallable_Check(_function)) {
    nassert_raise("Invalid function passed to PythonTask");
  }
}

/**
 * Replaces the argument list that is passed to the task function.  The
 * parameter should be a tuple or list of arguments, or None to indicate the
 * empty list.
 */
void PythonTask::
set_args(PyObject *args, bool append_task) {
  Py_XDECREF(_args);
  _args = nullptr;

  if (args == Py_None) {
    // None means no arguments; create an empty tuple.
    _args = PyTuple_New(0);
  } else {
    if (PySequence_Check(args)) {
      _args = PySequence_Tuple(args);
    }
  }

  if (_args == nullptr) {
    nassert_raise("Invalid args passed to PythonTask");
    _args = PyTuple_New(0);
  }

  _append_task = append_task;
}

/**
 * Returns the argument list that is passed to the task function.
 */
PyObject *PythonTask::
get_args() {
  if (_append_task) {
    // If we want to append the task, we have to create a new tuple with space
    // for one more at the end.  We have to do this dynamically each time, to
    // avoid storing the task itself in its own arguments list, and thereby
    // creating a cyclical reference.

    int num_args = PyTuple_GET_SIZE(_args);
    PyObject *with_task = PyTuple_New(num_args + 1);
    for (int i = 0; i < num_args; ++i) {
      PyObject *item = PyTuple_GET_ITEM(_args, i);
      PyTuple_SET_ITEM(with_task, i, Py_NewRef(item));
    }

    this->ref();
    PyObject *self = DTool_CreatePyInstance(this, Dtool_PythonTask, true, false);
    PyTuple_SET_ITEM(with_task, num_args, self);
    return with_task;
  }
  else {
    return Py_NewRef(_args);
  }
}

/**
 * Replaces the function that is called when the task finishes.  The parameter
 * should be a Python callable object.
 */
void PythonTask::
set_upon_death(PyObject *upon_death) {
  Py_XSETREF(_upon_death, Py_NewRef(upon_death));

  if (_upon_death != Py_None && !PyCallable_Check(_upon_death)) {
    nassert_raise("Invalid upon_death function passed to PythonTask");
  }
}

/**
 * Specifies a Python object that serves as the "owner" for the task.  This
 * owner object must have two methods: _addTask() and _clearTask(), which will
 * be called with one parameter, the task object.
 *
 * owner._addTask() is called when the task is added into the active task
 * list, and owner._clearTask() is called when it is removed.
 */
void PythonTask::
set_owner(PyObject *owner) {
#ifndef NDEBUG
  if (owner != Py_None) {
    PyObject *add = PyObject_GetAttrString(owner, "_addTask");
    PyErr_Clear();
    PyObject *clear = PyObject_GetAttrString(owner, "_clearTask");
    PyErr_Clear();

    bool valid_add = false;
    if (add != nullptr) {
      valid_add = PyCallable_Check(add);
      Py_DECREF(add);
    }
    bool valid_clear = false;
    if (clear != nullptr) {
      valid_clear = PyCallable_Check(clear);
      Py_DECREF(clear);
    }

    if (!valid_add || !valid_clear) {
      Dtool_Raise_TypeError("owner object should have _addTask and _clearTask methods");
      return;
    }
  }
#endif

  if (_owner != nullptr && _owner != Py_None && _state != S_inactive) {
    unregister_from_owner();
  }

  Py_XSETREF(_owner, Py_NewRef(owner));

  if (_owner != Py_None && _state != S_inactive) {
    register_to_owner();
  }
}

/**
 * Returns the result of this task's execution, as set by set_result() within
 * the task or returned from a coroutine added to the task manager.  If an
 * exception occurred within this task, it is raised instead.
 */
PyObject *PythonTask::
get_result() const {
  nassertr(done(), nullptr);

  if (_exception == nullptr) {
    // The result of the call is stored in _exc_value.
    return Py_XNewRef(_exc_value);
  }
  else {
    _retrieved_exception = true;
    PyErr_Restore(Py_NewRef(_exception), Py_XNewRef(_exc_value), Py_XNewRef(_exc_traceback));
    return nullptr;
  }
}

/**
 * If an exception occurred during execution of this task, returns it.  This
 * is only set if this task returned a coroutine or generator.
 */
/*PyObject *PythonTask::
exception() const {
  if (_exception == nullptr) {
    return Py_NewRef(Py_None);
  }
  else if (_exc_value == nullptr || _exc_value == Py_None) {
    return PyObject_CallNoArgs(_exception);
  }
  else if (PyTuple_Check(_exc_value)) {
    return PyObject_Call(_exception, _exc_value, nullptr);
  }
  else {
    return PyObject_CallOneArg(_exception, _exc_value);
  }
}*/

/**
 * Maps from an expression like "task.attr_name = v". This is customized here
 * so we can support some traditional task interfaces that supported directly
 * assigning certain values.  We also support adding arbitrary data to the
 * Task object.
 */
int PythonTask::
__setattr__(PyObject *self, PyObject *attr, PyObject *v) {
  if (!PyUnicode_Check(attr)) {
    PyErr_Format(PyExc_TypeError,
                 "attribute name must be string, not '%.200s'",
                 attr->ob_type->tp_name);
    return -1;
  }

  PyObject *descr = _PyType_Lookup(Py_TYPE(self), attr);
  if (descr != nullptr) {
    descrsetfunc f = descr->ob_type->tp_descr_set;
    if (f != nullptr) {
      return f(descr, self, v);
    }
  }

  if (task_cat.is_debug()) {
    PyObject *str = PyObject_Repr(v);
    task_cat.debug()
      << *this << ": task."
      << PyUnicode_AsUTF8(attr) << " = "
      << PyUnicode_AsUTF8(str) << "\n";
    Py_DECREF(str);
  }

  return PyDict_SetItem(__dict__, attr, v);
}

/**
 * Maps from an expression like "del task.attr_name". This is customized here
 * so we can support some traditional task interfaces that supported directly
 * assigning certain values.  We also support adding arbitrary data to the
 * Task object.
 */
int PythonTask::
__delattr__(PyObject *self, PyObject *attr) {
  if (PyObject_GenericSetAttr(self, attr, nullptr) == 0) {
    return 0;
  }

  if (!PyErr_ExceptionMatches(PyExc_AttributeError)) {
    return -1;
  }

  PyErr_Clear();

  if (PyDict_DelItem(__dict__, attr) == -1) {
    // PyDict_DelItem does not raise an exception.
    PyErr_Format(PyExc_AttributeError,
                 "'PythonTask' object has no attribute '%U'",
                 attr);
    return -1;
  }

  return 0;
}

/**
 * Maps from an expression like "task.attr_name". This is customized here so
 * we can support some traditional task interfaces that supported directly
 * querying certain values.  We also support adding arbitrary data to the Task
 * object.
 */
PyObject *PythonTask::
__getattribute__(PyObject *self, PyObject *attr) const {
  // We consult the instance dict first, since the user may have overridden a
  // method or something.
  PyObject *item = PyDict_GetItem(__dict__, attr);

  if (item != nullptr) {
    // PyDict_GetItem returns a borrowed reference.
    return Py_NewRef(item);
  }

  return PyObject_GenericGetAttr(self, attr);
}

/**
 * Called by Python to implement cycle detection.
 */
int PythonTask::
__traverse__(visitproc visit, void *arg) {
  Py_VISIT(__self__);
  Py_VISIT(_function);
  Py_VISIT(_args);
  Py_VISIT(_upon_death);
  Py_VISIT(_owner);
  Py_VISIT(__dict__);
  Py_VISIT(_generator);
  return 0;
}

/**
 * Called by Python to implement cycle breaking.
 */
int PythonTask::
__clear__() {
  Py_CLEAR(_function);
  Py_CLEAR(_args);
  Py_CLEAR(_upon_death);
  Py_CLEAR(_owner);
  Py_CLEAR(__dict__);
  Py_CLEAR(_generator);

  Py_CLEAR(__self__);
  return 0;
}

/**
 *
 */
bool PythonTask::
unref() const {
  if (!AsyncTask::unref()) {
    // It was cleaned up by the Python garbage collector.
    return false;
  }

  // If the last reference to the object is the one being held by Python,
  // check whether the Python wrapper itself is also at a refcount of 1.
  bool result = true;
  if (get_ref_count() == 1) {
    PyGILState_STATE gstate = PyGILState_Ensure();

    // Check whether we have a Python wrapper.  This is not the case if the
    // object has been created by C++ and never been exposed to Python code.
    PyObject *self = __self__;
    if (self != nullptr) {
      int ref_count = Py_REFCNT(self);
      assert(ref_count > 0);
      if (ref_count == 1) {
        // The last reference to the Python wrapper is being held by us.
        // Break the reference cycle and allow the object to go away.
        if (!AsyncTask::unref()) {
          PyObject_GC_UnTrack(self);
          ((Dtool_PyInstDef *)self)->_memory_rules = false;
          __self__ = nullptr;
          Py_DECREF(self);

          // Let the caller destroy the object.
          result = false;
        }
      }
    }
    PyGILState_Release(gstate);
  }
  return result;
}

/**
 * Cancels this task.  This is equivalent to remove(), except for coroutines,
 * for which it will throw an exception into any currently pending await.
 */
bool PythonTask::
cancel() {
  AsyncTaskManager *manager = _manager;
  if (manager != nullptr) {
    nassertr(_chain->_manager == manager, false);
    if (task_cat.is_debug()) {
      task_cat.debug()
        << "Cancelling " << *this << "\n";
    }

    bool must_cancel = true;
    if (_fut_waiter != nullptr) {
      // Cancel the future that this task is waiting on.  Note that we do this
      // before grabbing the lock, since this operation may also grab it.  This
      // means that _fut_waiter is only protected by the GIL.
#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
      // Use PyGILState to protect this asynchronous call.
      PyGILState_STATE gstate;
      gstate = PyGILState_Ensure();
#endif

      // Shortcut for unextended AsyncFuture.
      if (Py_TYPE(_fut_waiter) == (PyTypeObject *)&Dtool_AsyncFuture) {
        AsyncFuture *fut = (AsyncFuture *)DtoolInstance_VOID_PTR(_fut_waiter);
        if (!fut->done()) {
          fut->cancel();
        }
        if (fut->done()) {
          // We don't need this anymore.
          Py_DECREF(_fut_waiter);
          _fut_waiter = nullptr;
        }
      }
      else {
        PyObject *result = PyObject_CallMethod(_fut_waiter, "cancel", nullptr);
        Py_XDECREF(result);
      }

#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
      PyGILState_Release(gstate);
#endif
      // Keep _fut_waiter in any case, because we may need to cancel it again
      // later if it ignores the cancellation.
    }

    MutexHolder holder(manager->_lock);
    if (_state == S_awaiting) {
      // Reactivate it so that it can receive a CancelledException.
      if (must_cancel) {
        _must_cancel = true;
      }
      _state = AsyncTask::S_active;
      _chain->_active.push_back(this);
      --_chain->_num_awaiting_tasks;
      return true;
    }
    else if (must_cancel || _fut_waiter != nullptr) {
      // We may be polling an external future, so we still need to throw a
      // CancelledException and allow it to be caught.
      if (must_cancel) {
        _must_cancel = true;
      }
      return true;
    }
    else if (_chain->do_remove(this, true)) {
      return true;
    }
    else {
      if (task_cat.is_debug()) {
        task_cat.debug()
          << "  (unable to cancel " << *this << ")\n";
      }
      return false;
    }
  }

  return false;
}

/**
 * Override this function to return true if the task can be successfully
 * executed, false if it cannot.  Mainly intended as a sanity check when
 * attempting to add the task to a task manager.
 *
 * This function is called with the lock held.
 */
bool PythonTask::
is_runnable() {
  return _function != Py_None;
}

/**
 * Override this function to do something useful for the task.
 *
 * This function is called with the lock *not* held.
 */
AsyncTask::DoneStatus PythonTask::
do_task() {
#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
  // Use PyGILState to protect this asynchronous call.
  PyGILState_STATE gstate;
  gstate = PyGILState_Ensure();
#endif

  DoneStatus result = do_python_task();

#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
  PyGILState_Release(gstate);
#endif

  return result;
}

/**
 * The Python calls that implement do_task().  This function is separate so we
 * can acquire the Python interpretor lock while it runs.
 */
AsyncTask::DoneStatus PythonTask::
do_python_task() {
  PyObject *result = nullptr;

  // Are we waiting for a future to finish?  Short-circuit all the logic below
  // by simply calling done().
  {
    PyObject *fut_waiter = _fut_waiter;
    if (fut_waiter != nullptr) {
      PyObject *is_done = PyObject_CallMethod(fut_waiter, "done", nullptr);
      if (is_done == nullptr) {
        return DS_interrupt;
      }
      if (!PyObject_IsTrue(is_done)) {
        // Nope, ask again next frame.
        Py_DECREF(is_done);
        return DS_cont;
      }
      Py_DECREF(is_done);
      Py_DECREF(fut_waiter);
      _fut_waiter = nullptr;
    }
  }

  if (_generator == nullptr) {
    // We are calling the function directly.
    nassertr(_function != nullptr, DS_interrupt);

    PyObject *args = get_args();
    result = PythonThread::call_python_func(_function, args);
    Py_DECREF(args);

    if (result != nullptr && PyGen_Check(result)) {
      // The function has yielded a generator.  We will call into that
      // henceforth, instead of calling the function from the top again.
      if (task_cat.is_debug()) {
        PyObject *str = PyObject_ASCII(_function);
        task_cat.debug()
          << PyUnicode_AsUTF8(str) << " in " << *this
          << " yielded a generator.\n";
        Py_DECREF(str);
      }
      _generator = result;
      result = nullptr;

    } else if (result != nullptr && Py_TYPE(result)->tp_as_async != nullptr) {
      // The function yielded a coroutine, or something of the sort.
      if (task_cat.is_debug()) {
        PyObject *str = PyObject_ASCII(_function);
        PyObject *str2 = PyObject_ASCII(result);
        task_cat.debug()
          << PyUnicode_AsUTF8(str) << " in " << *this
          << " yielded an awaitable: " << PyUnicode_AsUTF8(str2) << "\n";
        Py_DECREF(str);
        Py_DECREF(str2);
      }
      if (PyCoro_CheckExact(result)) {
        // If a coroutine, am_await is possible but senseless, since we can
        // just call send(None) on the coroutine itself.
        _generator = result;
      } else {
        unaryfunc await = Py_TYPE(result)->tp_as_async->am_await;
        _generator = await(result);
        Py_DECREF(result);
      }
      result = nullptr;
    }
  }

  if (_generator != nullptr) {
    if (!_must_cancel) {
      // We are calling a generator.  Use "send" rather than PyIter_Next since
      // we need to be able to read the value from a StopIteration exception.
      PyObject *func = PyObject_GetAttrString(_generator, "send");
      nassertr(func != nullptr, DS_interrupt);
      result = PyObject_CallOneArg(func, Py_None);
      Py_DECREF(func);
    } else {
      // Throw a CancelledError into the generator.
      _must_cancel = false;
      PyObject *exc = PyObject_CallNoArgs(Extension<AsyncFuture>::get_cancelled_error_type());
      PyObject *func = PyObject_GetAttrString(_generator, "throw");
      result = PyObject_CallFunctionObjArgs(func, exc, nullptr);
      Py_DECREF(func);
      Py_DECREF(exc);
    }

    if (result == nullptr) {
      // An error happened.  If StopIteration, that indicates the task has
      // returned.  Otherwise, we need to save it so that it can be re-raised
      // in the function that awaited this task.
      Py_DECREF(_generator);
      _generator = nullptr;

#if PY_VERSION_HEX >= 0x030D0000 // Python 3.13
      // Python 3.13 does not support _PyGen_FetchStopIterationValue anymore.
      if (PyErr_ExceptionMatches(PyExc_StopIteration)) {
        PyObject *exc = PyErr_GetRaisedException();
        result = ((PyStopIterationObject *)exc)->value;
        if (result == nullptr) {
          result = Py_None;
        }
        result = Py_NewRef(result);
        Py_DECREF(exc);
#else
      if (_PyGen_FetchStopIterationValue(&result) == 0) {
#endif
        PyErr_Clear();

        if (_must_cancel) {
          // Task was cancelled right before finishing.  Make sure it is not
          // getting rerun or marked as successfully completed.
          _state = S_servicing_removed;
        }

        // If we passed a coroutine into the task, eg. something like:
        //   taskMgr.add(my_async_function())
        // then we cannot rerun the task, so the return value is always
        // assumed to be DS_done.  Instead, we pass the return value to the
        // result of the `await` expression.
        if (_function == nullptr) {
          if (task_cat.is_debug()) {
            task_cat.debug()
              << *this << " received StopIteration from coroutine.\n";
          }
          // Store the result in _exc_value because that's not used anyway.
          Py_XDECREF(_exc_value);
          _exc_value = result;
          return DS_done;
        }

      } else if (PyErr_ExceptionMatches(Extension<AsyncFuture>::get_cancelled_error_type())) {
        // Someone cancelled the coroutine, and it did not bother to handle it,
        // so we should consider it cancelled.
        if (task_cat.is_debug()) {
          task_cat.debug()
            << *this << " was cancelled and did not catch CancelledError.\n";
        }
        _state = S_servicing_removed;
        PyErr_Clear();
        return DS_done;

      } else if (_function == nullptr) {
        // We got an exception.  If this is a scheduled coroutine, we will
        // keep it and instead throw it into whatever 'awaits' this task.
        // Otherwise, fall through and handle it the regular way.
        Py_XDECREF(_exception);
        Py_XDECREF(_exc_value);
        Py_XDECREF(_exc_traceback);
        PyErr_Fetch(&_exception, &_exc_value, &_exc_traceback);
        _retrieved_exception = false;

        if (task_cat.is_debug()) {
          if (_exception != nullptr && Py_TYPE(_exception) == &PyType_Type) {
            task_cat.debug()
              << *this << " received " << ((PyTypeObject *)_exception)->tp_name << " from coroutine.\n";
          } else {
            task_cat.debug()
              << *this << " received exception from coroutine.\n";
          }
        }

        // Tell the task chain we want to kill ourselves.  We indicate this is
        // a "clean exit" because we still want to run the done callbacks on
        // exception.
        return DS_done;
      }

    } else if (result == Py_None) {
      // Bare yield means to continue next frame.
      Py_DECREF(result);
      return DS_cont;

    } else if (DtoolInstance_Check(result)) {
      // We are waiting for an AsyncFuture (eg. other task) to finish.
      AsyncFuture *fut = (AsyncFuture *)DtoolInstance_UPCAST(result, Dtool_AsyncFuture);
      if (fut != nullptr) {
        // Suspend execution of this task until this other task has completed.
        if (fut != (AsyncFuture *)this && !fut->done()) {
          if (fut->is_task()) {
            // This is actually a task, do we need to schedule it with the
            // manager?  This allows doing something like
            //   await Task.pause(1.0)
            // directly instead of having to do:
            //   await taskMgr.add(Task.pause(1.0))
            AsyncTask *task = (AsyncTask *)fut;
            if (!task->is_alive()) {
              _manager->add(task);
            }
          }
          if (fut->add_waiting_task(this)) {
            if (task_cat.is_debug()) {
              task_cat.debug()
                << *this << " is now awaiting <" << *fut << ">.\n";
            }
          } else {
            // The task is already done.  Continue at next opportunity.
            if (task_cat.is_debug()) {
              task_cat.debug()
                << *this << " would await <" << *fut << ">, were it not already done.\n";
            }
            Py_DECREF(result);
            return DS_cont;
          }
        } else {
          // This is an error.  If we wanted to be fancier we could also
          // detect deeper circular dependencies.
          task_cat.error()
            << *this << " cannot await itself\n";
        }
        // Store the Python object in case we need to cancel it (it may be a
        // subclass of AsyncFuture that overrides cancel() from Python)
        _fut_waiter = result;
        return DS_await;
      }
    } else {
      // We are waiting for a non-Panda future to finish.  We currently
      // implement this by checking every frame whether the future is done.
      PyObject *check = PyObject_GetAttrString(result, "_asyncio_future_blocking");
      if (check != nullptr && check != Py_None) {
        Py_DECREF(check);
        // Next frame, check whether this future is done.
        PyObject *fut_done = PyObject_GetAttrString(result, "done");
        if (fut_done == nullptr || !PyCallable_Check(fut_done)) {
          Py_XDECREF(fut_done);
          task_cat.error()
            << "future.done is not callable\n";
          return DS_interrupt;
        }
        if (task_cat.is_debug()) {
          PyObject *str = PyObject_ASCII(result);
          task_cat.debug()
            << *this << " is now polling " << PyUnicode_AsUTF8(str) << ".done()\n";
          Py_DECREF(str);
        }
        Py_DECREF(fut_done);
        _fut_waiter = result;
        return DS_cont;
      }
      PyErr_Clear();
      Py_XDECREF(check);
    }
  }

  if (result == nullptr) {
    if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_SystemExit)) {
      // Don't print an error message for SystemExit.  Or rather, make it a
      // debug message.
      if (task_cat.is_debug()) {
        task_cat.debug()
          << "SystemExit occurred in " << *this << "\n";
      }
    } else {
      task_cat.error()
        << "Exception occurred in " << *this << "\n";
    }
    return DS_interrupt;
  }

  if (result == Py_None || _ignore_return) {
    Py_DECREF(result);
    return DS_done;
  }

  if (PyLong_Check(result)) {
    long retval = PyLong_AS_LONG(result);

    switch (retval) {
    case DS_again:
      Py_XDECREF(_generator);
      _generator = nullptr;
      // Fall through.

    case DS_done:
    case DS_cont:
    case DS_pickup:
    case DS_exit:
    case DS_pause:
      // Legitimate value.
      Py_DECREF(result);
      return (DoneStatus) retval;

    case -1:
      // Legacy value.
      Py_DECREF(result);
      return DS_done;

    default:
      // Unexpected value.
      break;
    }
  }

  // This is unfortunate, but some are returning task.done, which nowadays
  // conflicts with the AsyncFuture method.  Check if that is being returned.
  PyMethodDef *meth = nullptr;
  if (PyCFunction_Check(result)) {
    meth = ((PyCFunctionObject *)result)->m_ml;
  } else if (Py_TYPE(result) == &PyMethodDescr_Type) {
    meth = ((PyMethodDescrObject *)result)->d_method;
  }

  if (meth != nullptr && strcmp(meth->ml_name, "done") == 0) {
    Py_DECREF(result);
    return DS_done;
  }

  std::ostringstream strm;
  PyObject *str = PyObject_ASCII(result);
  if (str == nullptr) {
    str = PyUnicode_FromString("<repr error>");
  }
  strm
    << *this << " returned " << PyUnicode_AsUTF8(str);
  Py_DECREF(str);
  Py_DECREF(result);
  std::string message = strm.str();
  nassert_raise(message);

  return DS_interrupt;
}

/**
 * Override this function to do something useful when the task has been added
 * to the active queue.
 *
 * This function is called with the lock *not* held.
 */
void PythonTask::
upon_birth(AsyncTaskManager *manager) {
  AsyncTask::upon_birth(manager);
  register_to_owner();
}

/**
 * Override this function to do something useful when the task has been
 * removed from the active queue.  The parameter clean_exit is true if the
 * task has been removed because it exited normally (returning DS_done), or
 * false if it was removed for some other reason (e.g.
 * AsyncTaskManager::remove()).  By the time this method is called, _manager
 * has been cleared, so the parameter manager indicates the original
 * AsyncTaskManager that owned this task.
 *
 * The normal behavior is to throw the done_event only if clean_exit is true.
 *
 * This function is called with the lock *not* held.
 */
void PythonTask::
upon_death(AsyncTaskManager *manager, bool clean_exit) {
  AsyncTask::upon_death(manager, clean_exit);

  // If we were polling something when we were removed, get rid of it.
  //TODO: should we call cancel() on it?
  if (_fut_waiter != nullptr) {
    Py_DECREF(_fut_waiter);
    _fut_waiter = nullptr;
  }

  if (_upon_death != Py_None) {
#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
    // Use PyGILState to protect this asynchronous call.
    PyGILState_STATE gstate;
    gstate = PyGILState_Ensure();
#endif

    call_function(_upon_death);

#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
    PyGILState_Release(gstate);
#endif
  }
  unregister_from_owner();
}

/**
 * Tells the owner we are now his task.
 */
void PythonTask::
register_to_owner() {
  if (_owner != Py_None && !_registered_to_owner) {
#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
    // Use PyGILState to protect this asynchronous call.
    PyGILState_STATE gstate;
    gstate = PyGILState_Ensure();
#endif

    _registered_to_owner = true;
    call_owner_method("_addTask");

#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
    PyGILState_Release(gstate);
#endif
  }
}

/**
 * Tells the owner we are no longer his task.
 */
void PythonTask::
unregister_from_owner() {
  // make sure every call to _clearTask corresponds to a call to _addTask
  if (_owner != Py_None && _registered_to_owner) {
#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
    // Use PyGILState to protect this asynchronous call.
    PyGILState_STATE gstate;
    gstate = PyGILState_Ensure();
#endif

    _registered_to_owner = false;
    call_owner_method("_clearTask");

#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
    PyGILState_Release(gstate);
#endif
  }
}

/**
 * Calls the indicated method name on the given object, if defined, passing in
 * the task object as the only parameter.
 */
void PythonTask::
call_owner_method(const char *method_name) {
  if (_owner != Py_None) {
    PyObject *func = PyObject_GetAttrString(_owner, (char *)method_name);
    if (func == nullptr) {
      task_cat.error()
        << "Owner object added to " << *this << " has no method "
        << method_name << "().\n";

    } else {
      call_function(func);
      Py_DECREF(func);
    }
  }
}

/**
 * Calls the indicated Python function, passing in the task object as the only
 * parameter.
 */
void PythonTask::
call_function(PyObject *function) {
  if (function != Py_None) {
    this->ref();
    PyObject *self = DTool_CreatePyInstance(this, Dtool_PythonTask, true, false);
    PyObject *result = PyObject_CallOneArg(function, self);
    Py_XDECREF(result);
    Py_DECREF(self);
  }
}

#endif  // HAVE_PYTHON
