// Copyright David Abrahams 2001.
// Distributed under the Boost Software License, Version 1.0. (See
// accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

#include <boost/python/detail/prefix.hpp>
#include <boost/mpl/lambda.hpp> // #including this first is an intel6 workaround

#include <boost/python/object/class.hpp>
#include <boost/python/object/instance.hpp>
#include <boost/python/object/class_detail.hpp>
#include <boost/python/scope.hpp>
#include <boost/python/converter/registry.hpp>
#include <boost/python/object/find_instance.hpp>
#include <boost/python/object/pickle_support.hpp>
#include <boost/python/detail/map_entry.hpp>
#include <boost/python/object.hpp>
#include <boost/python/object_protocol.hpp>
#include <boost/detail/binary_search.hpp>
#include <boost/python/self.hpp>
#include <boost/python/dict.hpp>
#include <boost/python/str.hpp>
#include <boost/python/ssize_t.hpp>
#include <functional>
#include <vector>
#include <cstddef>
#include <new>
#include <structmember.h>

namespace boost { namespace python {

# ifdef BOOST_PYTHON_SELF_IS_CLASS
namespace self_ns
{
  self_t self;
}
# endif 

instance_holder::instance_holder()
    : m_next(0)
{
}

instance_holder::~instance_holder()
{
}

extern "C"
{
  // This is copied from typeobject.c in the Python sources. Even though
  // class_metatype_object doesn't set Py_TPFLAGS_HAVE_GC, that bit gets
  // filled in by the base class initialization process in
  // PyType_Ready(). However, tp_is_gc is *not* copied from the base
  // type, making it assume that classes are GC-able even if (like
  // class_type_object) they're statically allocated.
  static int
  type_is_gc(PyTypeObject *python_type)
  {
      return python_type->tp_flags & Py_TPFLAGS_HEAPTYPE;
  }

  // This is also copied from the Python sources.  We can't implement
  // static_data as a subclass property effectively without it.
  typedef struct {
      PyObject_HEAD
      PyObject *prop_get;
      PyObject *prop_set;
      PyObject *prop_del;
      PyObject *prop_doc;
      int getter_doc;
  } propertyobject;

  // Copied from Python source and removed the part for setting docstring,
  // since we don't have a setter for __doc__ and trying to set it will
  // cause the init fail.
  static int property_init(PyObject *self, PyObject *args, PyObject *kwds)
  {
      PyObject *get = NULL, *set = NULL, *del = NULL, *doc = NULL;
      static char *kwlist[] = {"fget", "fset", "fdel", "doc", 0};
      propertyobject *prop = (propertyobject *)self;

      if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO:property",
                  kwlist, &get, &set, &del, &doc))
          return -1;

      if (get == Py_None)
          get = NULL;
      if (set == Py_None)
          set = NULL;
      if (del == Py_None)
          del = NULL;

      Py_XINCREF(get);
      Py_XINCREF(set);
      Py_XINCREF(del);
      Py_XINCREF(doc);

      prop->prop_get = get;
      prop->prop_set = set;
      prop->prop_del = del;
      prop->prop_doc = doc;
      prop->getter_doc = 0;

      return 0;
  }

 
  static PyObject *
  static_data_descr_get(PyObject *self, PyObject * /*obj*/, PyObject * /*type*/)
  {
      propertyobject *gs = (propertyobject *)self;

      return PyObject_CallFunction(gs->prop_get, const_cast<char*>("()"));
  }

  static int
  static_data_descr_set(PyObject *self, PyObject * /*obj*/, PyObject *value)
  {
      propertyobject *gs = (propertyobject *)self;
      PyObject *func, *res;

      if (value == NULL)
          func = gs->prop_del;
      else
          func = gs->prop_set;
      if (func == NULL) {
          PyErr_SetString(PyExc_AttributeError,
                          value == NULL ?
                          "can't delete attribute" :
                          "can't set attribute");
          return -1;
      }
      if (value == NULL)
          res = PyObject_CallFunction(func, const_cast<char*>("()"));
      else
          res = PyObject_CallFunction(func, const_cast<char*>("(O)"), value);
      if (res == NULL)
          return -1;
      Py_DECREF(res);
      return 0;
  }
}

static PyTypeObject static_data_object = {
    PyVarObject_HEAD_INIT(NULL, 0)
    const_cast<char*>("Boost.Python.StaticProperty"),
    sizeof(propertyobject),
    0,
    0,                                      /* 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 */
    0,                                      /* tp_call */
    0,                                      /* tp_str */
    0,                                      /* tp_getattro */
    0,                                      /* tp_setattro */
    0,                                      /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT // | Py_TPFLAGS_HAVE_GC
    | Py_TPFLAGS_BASETYPE,          /* tp_flags */
    0,                                      /* tp_doc */
    0,                                      /* tp_traverse */
    0,                                      /* tp_clear */
    0,                                      /* tp_richcompare */
    0,                                      /* tp_weaklistoffset */
    0,                                      /* tp_iter */
    0,                                      /* tp_iternext */
    0,                                      /* tp_methods */
    0,                                      /* tp_members */
    0,                                      /* tp_getset */
    0, //&PyProperty_Type,                           /* tp_base */
    0,                                      /* tp_dict */
    static_data_descr_get,                                      /* tp_descr_get */
    static_data_descr_set,                                      /* tp_descr_set */
    0,                                      /* tp_dictoffset */
    property_init,                                      /* tp_init */
    0,                                      /* tp_alloc */
    0, // filled in with type_new           /* tp_new */
    0, // filled in with __PyObject_GC_Del  /* tp_free */
    0,                                      /* tp_is_gc */
    0,                                      /* tp_bases */
    0,                                      /* tp_mro */
    0,                                      /* tp_cache */
    0,                                      /* tp_subclasses */
    0,                                      /* tp_weaklist */
#if PYTHON_API_VERSION >= 1012
    0                                       /* tp_del */
#endif
};

namespace objects
{
#if PY_VERSION_HEX < 0x03000000
  // XXX Not sure why this run into compiling error in Python 3
  extern "C"
  {
      // This declaration needed due to broken Python 2.2 headers
      extern DL_IMPORT(PyTypeObject) PyProperty_Type;
  }
#endif

  BOOST_PYTHON_DECL PyObject* static_data()
  {
      if (static_data_object.tp_dict == 0)
      {
          Py_TYPE(&static_data_object) = &PyType_Type;
          static_data_object.tp_base = &PyProperty_Type;
          if (PyType_Ready(&static_data_object))
              return 0;
      }
      return upcast<PyObject>(&static_data_object);
  }
}  

extern "C"
{
    // Ordinarily, descriptors have a certain assymetry: you can use
    // them to read attributes off the class object they adorn, but
    // writing the same attribute on the class object always replaces
    // the descriptor in the class __dict__.  In order to properly
    // represent C++ static data members, we need to allow them to be
    // written through the class instance.  This function of the
    // metaclass makes it possible.
    static int
    class_setattro(PyObject *obj, PyObject *name, PyObject* value)
    {
        // Must use "private" Python implementation detail
        // _PyType_Lookup instead of PyObject_GetAttr because the
        // latter will always end up calling the descr_get function on
        // any descriptor it finds; we need the unadulterated
        // descriptor here.
        PyObject* a = _PyType_Lookup(downcast<PyTypeObject>(obj), name);

        // a is a borrowed reference or 0
        
        // If we found a static data descriptor, call it directly to
        // force it to set the static data member
        if (a != 0 && PyObject_IsInstance(a, objects::static_data()))
            return Py_TYPE(a)->tp_descr_set(a, obj, value);
        else
            return PyType_Type.tp_setattro(obj, name, value);
    }
}

static PyTypeObject class_metatype_object = {
    PyVarObject_HEAD_INIT(NULL, 0)
    const_cast<char*>("Boost.Python.class"),
    PyType_Type.tp_basicsize,
    0,
    0,                                      /* 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 */
    0,                                      /* tp_call */
    0,                                      /* tp_str */
    0,                                      /* tp_getattro */
    class_setattro,                         /* tp_setattro */
    0,                                      /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT // | Py_TPFLAGS_HAVE_GC
    | Py_TPFLAGS_BASETYPE,          /* tp_flags */
    0,                                      /* tp_doc */
    0,                                      /* tp_traverse */
    0,                                      /* tp_clear */
    0,                                      /* tp_richcompare */
    0,                                      /* tp_weaklistoffset */
    0,                                      /* tp_iter */
    0,                                      /* tp_iternext */
    0,                                      /* tp_methods */
    0,                                      /* tp_members */
    0,                                      /* tp_getset */
    0, //&PyType_Type,                           /* tp_base */
    0,                                      /* tp_dict */
    0,                                      /* tp_descr_get */
    0,                                      /* tp_descr_set */
    0,                                      /* tp_dictoffset */
    0,                                      /* tp_init */
    0,                                      /* tp_alloc */
    0, // filled in with type_new           /* tp_new */
    0, // filled in with __PyObject_GC_Del  /* tp_free */
    (inquiry)type_is_gc,                    /* tp_is_gc */
    0,                                      /* tp_bases */
    0,                                      /* tp_mro */
    0,                                      /* tp_cache */
    0,                                      /* tp_subclasses */
    0,                                      /* tp_weaklist */
#if PYTHON_API_VERSION >= 1012
    0                                       /* tp_del */
#endif
};

// Install the instance data for a C++ object into a Python instance
// object.
void instance_holder::install(PyObject* self) throw()
{
    assert(Py_TYPE(Py_TYPE(self)) == &class_metatype_object);
    m_next = ((objects::instance<>*)self)->objects;
    ((objects::instance<>*)self)->objects = this;
}


namespace objects
{
// Get the metatype object for all extension classes.
  BOOST_PYTHON_DECL type_handle class_metatype()
  {
      if (class_metatype_object.tp_dict == 0)
      {
          Py_TYPE(&class_metatype_object) = &PyType_Type;
          class_metatype_object.tp_base = &PyType_Type;
          if (PyType_Ready(&class_metatype_object))
              return type_handle();
      }
      return type_handle(borrowed(&class_metatype_object));
  }
  extern "C"
  {
      static void instance_dealloc(PyObject* inst)
      {
          instance<>* kill_me = (instance<>*)inst;

          for (instance_holder* p = kill_me->objects, *next; p != 0; p = next)
          {
              next = p->next();
              p->~instance_holder();
              instance_holder::deallocate(inst, dynamic_cast<void*>(p));
          }
        
          // Python 2.2.1 won't add weak references automatically when
          // tp_itemsize > 0, so we need to manage that
          // ourselves. Accordingly, we also have to clean up the
          // weakrefs ourselves.
          if (kill_me->weakrefs != NULL)
            PyObject_ClearWeakRefs(inst);

          Py_XDECREF(kill_me->dict);
          
          Py_TYPE(inst)->tp_free(inst);
      }

      static PyObject *
      instance_new(PyTypeObject* type_, PyObject* /*args*/, PyObject* /*kw*/)
      {
          // Attempt to find the __instance_size__ attribute. If not present, no problem.
          PyObject* d = type_->tp_dict;
          PyObject* instance_size_obj = PyObject_GetAttrString(d, const_cast<char*>("__instance_size__"));

          ssize_t instance_size = instance_size_obj ? 
#if PY_VERSION_HEX >= 0x03000000
              PyLong_AsSsize_t(instance_size_obj) : 0;
#else
              PyInt_AsLong(instance_size_obj) : 0;
#endif
          
          if (instance_size < 0)
              instance_size = 0;
          
          PyErr_Clear(); // Clear any errors that may have occurred.

          instance<>* result = (instance<>*)type_->tp_alloc(type_, instance_size);
          if (result)
          {
              // Guido says we can use ob_size for any purpose we
              // like, so we'll store the total size of the object
              // there. A negative number indicates that the extra
              // instance memory is not yet allocated to any holders.
#if PY_VERSION_HEX >= 0x02060000
              Py_SIZE(result) =
#else
              result->ob_size =
#endif
                  -(static_cast<int>(offsetof(instance<>,storage) + instance_size));
          }
          return (PyObject*)result;
      }

      static PyObject* instance_get_dict(PyObject* op, void*)
      {
          instance<>* inst = downcast<instance<> >(op);
          if (inst->dict == 0)
              inst->dict = PyDict_New();
          return python::xincref(inst->dict);
      }
    
      static int instance_set_dict(PyObject* op, PyObject* dict, void*)
      {
          instance<>* inst = downcast<instance<> >(op);
          python::xdecref(inst->dict);
          inst->dict = python::incref(dict);
          return 0;
      }

  }


  static PyGetSetDef instance_getsets[] = {
      {const_cast<char*>("__dict__"),  instance_get_dict,  instance_set_dict, NULL, 0},
      {0, 0, 0, 0, 0}
  };

  
  static PyMemberDef instance_members[] = {
      {const_cast<char*>("__weakref__"), T_OBJECT, offsetof(instance<>, weakrefs), 0, 0},
      {0, 0, 0, 0, 0}
  };

  static PyTypeObject class_type_object = {
      PyVarObject_HEAD_INIT(NULL, 0)
      const_cast<char*>("Boost.Python.instance"),
      offsetof(instance<>,storage),           /* tp_basicsize */
      1,                                      /* tp_itemsize */
      instance_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 */
      0,                                      /* tp_call */
      0,                                      /* tp_str */
      0,                                      /* tp_getattro */
      0,                                      /* tp_setattro */
      0,                                      /* tp_as_buffer */
      Py_TPFLAGS_DEFAULT // | Py_TPFLAGS_HAVE_GC
      | Py_TPFLAGS_BASETYPE,          /* tp_flags */
      0,                                      /* tp_doc */
      0,                                      /* tp_traverse */
      0,                                      /* tp_clear */
      0,                                      /* tp_richcompare */
      offsetof(instance<>,weakrefs),          /* tp_weaklistoffset */
      0,                                      /* tp_iter */
      0,                                      /* tp_iternext */
      0,                                      /* tp_methods */
      instance_members,                       /* tp_members */
      instance_getsets,                       /* tp_getset */
      0, //&PyBaseObject_Type,                /* tp_base */
      0,                                      /* tp_dict */
      0,                                      /* tp_descr_get */
      0,                                      /* tp_descr_set */
      offsetof(instance<>,dict),              /* tp_dictoffset */
      0,                                      /* tp_init */
      PyType_GenericAlloc,                    /* tp_alloc */
      instance_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 */
#if PYTHON_API_VERSION >= 1012
      0                                       /* tp_del */
#endif
  };

  BOOST_PYTHON_DECL type_handle class_type()
  {
      if (class_type_object.tp_dict == 0)
      {
          Py_TYPE(&class_type_object) = incref(class_metatype().get());
          class_type_object.tp_base = &PyBaseObject_Type;
          if (PyType_Ready(&class_type_object))
              return type_handle();
//          class_type_object.tp_setattro = class_setattro;
      }
      return type_handle(borrowed(&class_type_object));
  }

  BOOST_PYTHON_DECL void*
  find_instance_impl(PyObject* inst, type_info type, bool null_shared_ptr_only)
  {
      if (Py_TYPE(Py_TYPE(inst)) != &class_metatype_object)
          return 0;
    
      instance<>* self = reinterpret_cast<instance<>*>(inst);

      for (instance_holder* match = self->objects; match != 0; match = match->next())
      {
          void* const found = match->holds(type, null_shared_ptr_only);
          if (found)
              return found;
      }
      return 0;
  }

  object module_prefix()
  {
      return object(
          PyObject_IsInstance(scope().ptr(), upcast<PyObject>(&PyModule_Type))
          ? object(scope().attr("__name__"))
          : api::getattr(scope(), "__module__", str())
          );
  }

  namespace
  {
    // Find a registered class object corresponding to id. Return a
    // null handle if no such class is registered.
    inline type_handle query_class(type_info id)
    {
        converter::registration const* p = converter::registry::query(id);
        return type_handle(
            python::borrowed(
                python::allow_null(p ? p->m_class_object : 0))
            );
    }

    // Find a registered class corresponding to id. If not found,
    // throw an appropriate exception.
    type_handle get_class(type_info id)
    {
        type_handle result(query_class(id));

        if (result.get() == 0)
        {
            object report("extension class wrapper for base class ");
            report = report + id.name() + " has not been created yet";
            PyErr_SetObject(PyExc_RuntimeError, report.ptr());
            throw_error_already_set();
        }
        return result;
    }

    // class_base constructor
    //
    // name -      the name of the new Python class
    //
    // num_types - one more than the number of declared bases
    //
    // types -     array of python::type_info, the first item
    //             corresponding to the class being created, and the
    //             rest corresponding to its declared bases.
    //
    inline object
    new_class(char const* name, std::size_t num_types, type_info const* const types, char const* doc)
    {
      assert(num_types >= 1);
      
      // Build a tuple of the base Python type objects. If no bases
      // were declared, we'll use our class_type() as the single base
      // class.
      ssize_t const num_bases = (std::max)(num_types - 1, static_cast<std::size_t>(1));
      handle<> bases(PyTuple_New(num_bases));

      for (ssize_t i = 1; i <= num_bases; ++i)
      {
          type_handle c = (i >= static_cast<ssize_t>(num_types)) ? class_type() : get_class(types[i]);
          // PyTuple_SET_ITEM steals this reference
          PyTuple_SET_ITEM(bases.get(), static_cast<ssize_t>(i - 1), upcast<PyObject>(c.release()));
      }

      // Call the class metatype to create a new class
      dict d;
   
      object m = module_prefix();
      if (m) d["__module__"] = m;

      if (doc != 0)
          d["__doc__"] = doc;
      
      object result = object(class_metatype())(name, bases, d);
      assert(PyType_IsSubtype(Py_TYPE(result.ptr()), &PyType_Type));
      
      if (scope().ptr() != Py_None)
          scope().attr(name) = result;

      // For pickle. Will lead to informative error messages if pickling
      // is not enabled.
      result.attr("__reduce__") = object(make_instance_reduce_function());

      return result;
    }
  }
  
  class_base::class_base(
      char const* name, std::size_t num_types, type_info const* const types, char const* doc)
      : object(new_class(name, num_types, types, doc))
  {
      // Insert the new class object in the registry
      converter::registration& converters = const_cast<converter::registration&>(
          converter::registry::lookup(types[0]));

      // Class object is leaked, for now
      converters.m_class_object = (PyTypeObject*)incref(this->ptr());
  }

  BOOST_PYTHON_DECL void copy_class_object(type_info const& src, type_info const& dst)
  {
      converter::registration& dst_converters
          = const_cast<converter::registration&>(converter::registry::lookup(dst));
      
      converter::registration const& src_converters = converter::registry::lookup(src);

      dst_converters.m_class_object = src_converters.m_class_object;
  }
  
  void class_base::set_instance_size(std::size_t instance_size)
  {
      this->attr("__instance_size__") = instance_size;
  }
  
  void class_base::add_property(
    char const* name, object const& fget, char const* docstr)
  {
      object property(
          (python::detail::new_reference)
          PyObject_CallFunction((PyObject*)&PyProperty_Type, const_cast<char*>("Osss"), fget.ptr(), 0, 0, docstr));
      
      this->setattr(name, property);
  }

  void class_base::add_property(
    char const* name, object const& fget, object const& fset, char const* docstr)
  {
      object property(
          (python::detail::new_reference)
          PyObject_CallFunction((PyObject*)&PyProperty_Type, const_cast<char*>("OOss"), fget.ptr(), fset.ptr(), 0, docstr));
      
      this->setattr(name, property);
  }

  void class_base::add_static_property(char const* name, object const& fget)
  {
      object property(
          (python::detail::new_reference) 
          PyObject_CallFunction(static_data(), const_cast<char*>("O"), fget.ptr())
          );
      
      this->setattr(name, property);
  }

  void class_base::add_static_property(char const* name, object const& fget, object const& fset)
  {
      object property(
          (python::detail::new_reference)
          PyObject_CallFunction(static_data(), const_cast<char*>("OO"), fget.ptr(), fset.ptr()));
      
      this->setattr(name, property);
  }

  void class_base::setattr(char const* name, object const& x)
  {
      if (PyObject_SetAttrString(this->ptr(), const_cast<char*>(name), x.ptr()) < 0)
          throw_error_already_set();
  }

  namespace
  {
    extern "C" PyObject* no_init(PyObject*, PyObject*)
    {
        ::PyErr_SetString(::PyExc_RuntimeError, const_cast<char*>("This class cannot be instantiated from Python"));
        return NULL;
    }
    static ::PyMethodDef no_init_def = {
        const_cast<char*>("__init__"), no_init, METH_VARARGS,
        const_cast<char*>("Raises an exception\n"
                          "This class cannot be instantiated from Python\n")
    };
  }
  
  void class_base::def_no_init()
  {
      handle<> f(::PyCFunction_New(&no_init_def, 0));
      this->setattr("__init__", object(f));
  }

  void class_base::enable_pickling_(bool getstate_manages_dict)
  {
      setattr("__safe_for_unpickling__", object(true));
      
      if (getstate_manages_dict)
      {
          setattr("__getstate_manages_dict__", object(true));
      }
  }

  namespace
  {
    PyObject* callable_check(PyObject* callable)
    {
        if (PyCallable_Check(expect_non_null(callable)))
            return callable;

        ::PyErr_Format(
            PyExc_TypeError
          , const_cast<char*>("staticmethod expects callable object; got an object of type %s, which is not callable")
            , Py_TYPE(callable)->tp_name
            );
        
        throw_error_already_set();
        return 0;
    }
  }
  
  void class_base::make_method_static(const char * method_name)
  {
      PyTypeObject* self = downcast<PyTypeObject>(this->ptr());
      dict d((handle<>(borrowed(self->tp_dict))));

      object method(d[method_name]);

      this->attr(method_name) = object(
          handle<>(
              PyStaticMethod_New((callable_check)(method.ptr()) )
              ));
  }

  BOOST_PYTHON_DECL type_handle registered_class_object(type_info id)
  {
      return query_class(id);
  }
} // namespace objects


void* instance_holder::allocate(PyObject* self_, std::size_t holder_offset, std::size_t holder_size)
{
    assert(Py_TYPE(Py_TYPE(self_)) == &class_metatype_object);
    objects::instance<>* self = (objects::instance<>*)self_;
    
    int total_size_needed = holder_offset + holder_size;
    
    if (-Py_SIZE(self) >= total_size_needed)
    {
        // holder_offset should at least point into the variable-sized part
        assert(holder_offset >= offsetof(objects::instance<>,storage));

        // Record the fact that the storage is occupied, noting where it starts
        Py_SIZE(self) = holder_offset;
        return (char*)self + holder_offset;
    }
    else
    {
        void* const result = PyMem_Malloc(holder_size);
        if (result == 0)
            throw std::bad_alloc();
        return result;
    }
}

void instance_holder::deallocate(PyObject* self_, void* storage) throw()
{
    assert(Py_TYPE(Py_TYPE(self_)) == &class_metatype_object);
    objects::instance<>* self = (objects::instance<>*)self_;
    if (storage != (char*)self + Py_SIZE(self))
    {
        PyMem_Free(storage);
    }
}

}} // namespace boost::python

