#pragma once

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <structmember.h>

#include "typeattrs.h"
#include "exceptions.h"
#include "module.h"

namespace NPyBind {
    void RegisterJSONBridge();

    namespace NPrivate {
        template <typename>
        class TUnboundClosureHolder;
        template <typename>
        class TUnboundClosure;
    }

    // TTraits should be derived from TPythonType
    template <typename TObjectHolder, typename TObject, typename TTraits>
    class TPythonType {
    private:
        TPythonType(const TPythonType&);
        TPythonType& operator=(const TPythonType&);

    private:
        typedef typename TPythonTypeAttributes<TObject>::TGetterPtr TGetterPtr;
        typedef typename TPythonTypeAttributes<TObject>::TSetterPtr TSetterPtr;
        typedef typename TPythonTypeAttributes<TObject>::TCallerPtr TCallerPtr;

        struct TProxy {
            PyObject_HEAD
                TObjectHolder* Holder;
        };

        static PyTypeObject PyType;
        static PyMappingMethods MappingMethods;
        static PyObject* PyTypeObjPtr;
    protected:
        static PyTypeObject* GetPyTypePtr() {
            return &PyType;
        }
    private:

        TPythonTypeAttributes<TObject> Attributes;

        static int InitObject(PyObject* s, PyObject* args, PyObject* kwargs) {
            try {
                TProxy* self = reinterpret_cast<TProxy*>(s);
                auto str = NameFromString("__properties__");
                if (kwargs && PyDict_Check(kwargs) && PyDict_Contains(kwargs, str.Get())) {
                    TPyObjectPtr props(PyDict_GetItem(kwargs, str.Get()));
                    TVector<TString> properties;
                    FromPyObject(props.Get(), properties);
                    self->Holder = TTraits::DoInitPureObject(properties);
                } else {
                    self->Holder = (args || kwargs) ? TTraits::DoInitObject(args, kwargs) : nullptr;
                }
                if (PyErr_Occurred())
                    return -1;
                return 0;
            } catch (const std::exception& ex) {
                PyErr_SetString(TExceptionsHolder::Instance().ToPyException(ex).Get(), ex.what());
            } catch (...) {
                PyErr_SetString(PyExc_RuntimeError, "Unknown error occurred while trying to init object");
            }
            return -1;
        }

        static void DeallocObject(TProxy* self) {
            delete self->Holder;
            Py_TYPE(self)->tp_free(reinterpret_cast<PyObject*>(self));
        }

        static PyObject* GetObjectAttr(PyObject* pyObj, char* attr);
        static int SetObjectAttr(PyObject* pyObj, char* attr, PyObject* value);
        static PyObject* GetStr(PyObject*);
        static PyObject* GetRepr(PyObject*);
        static PyObject* GetIter(PyObject*);
        static PyObject* GetNext(PyObject*);

        // Fill class __dict__ with functions to make sure methods names will get to dir()
        void FillClassDict() const {
            TVector<TString> names;
            Attributes.GetMethodCallers().GetAllMethodsNames(names);
            for (const auto& name : names) {
                TPyObjectPtr callable = NPrivate::TUnboundClosure<TObject>::Instance().CreatePyObject(new NPrivate::TUnboundClosureHolder<TObject>(&PyType, name));
                PyDict_SetItemString(PyType.tp_dict, name.c_str(), callable.Get());
            }
        }

        void InitCommonAttributes() {
            static bool was = false;
            if (was)
                return;
            was = true;
            Attributes.InitCommonAttributes();
            FillClassDict();
        }

    protected:
        TPythonType(const char* pyTypeName, const char* typeDescr, PyTypeObject* parentType = nullptr)
            : Attributes(GetObjectAttr, SetObjectAttr)
        {
            PyType.tp_name = pyTypeName;
            PyType.tp_doc = typeDescr;
            Py_INCREF(PyTypeObjPtr);
            if (parentType) {
                Py_INCREF(parentType);
                PyType.tp_base = parentType;
            }
            PyType_Ready(&PyType);

            TExceptionsHolder::Instance();
            RegisterJSONBridge();

        }

        ~TPythonType() {
        }

        static TObjectHolder* DoInitObject(PyObject*, PyObject*) {
            return nullptr;
        }

        static TObjectHolder* DoInitPureObject(const TVector<TString>&) {
            return nullptr;
        }

        static void SetClosure(PyObject* (*call)(PyObject*, PyObject*, PyObject*)) {
            PyType.tp_call = call;
        }

    public:
        void AddGetter(const TString& attr, TGetterPtr getter) {
            Attributes.AddGetter(attr, getter);
        }

        void AddSetter(const TString& attr, TSetterPtr setter) {
            Attributes.AddSetter(attr, setter);
        }

        void AddCaller(const TString& name, TCallerPtr caller) {
            Attributes.AddCaller(name, caller);
            if (name == "__iter__") {
                PyType.tp_iter = GetIter;
            }
            if (name == "next") {
                PyType.tp_iternext = GetNext;
            }
        }

        void SetIter(getiterfunc tp_iter) {
            PyType.tp_iter = tp_iter;
        }

        void SetIterNext(iternextfunc tp_iternext) {
            PyType.tp_iternext = tp_iternext;
        }

        void SetDestructor(destructor tp_dealloc) {
            PyType.tp_dealloc = tp_dealloc;
        }

        void SetLengthFunction(lenfunc mp_length) {
            PyType.tp_as_mapping->mp_length = mp_length;
        }

        void SetSubscriptFunction(binaryfunc mp_subscript) {
            PyType.tp_as_mapping->mp_subscript = mp_subscript;
        }

        void SetAssSubscriptFunction(objobjargproc mp_ass_subscript) {
            PyType.tp_as_mapping->mp_ass_subscript = mp_ass_subscript;
        }

        typedef TObject TObjectType;

        static TPythonType& Instance() {
            static TTraits Traits;
            Traits.InitCommonAttributes();
            return Traits;
        }

        void Register(PyObject* module, const char* typeName) {
            Py_INCREF(PyTypeObjPtr);
            if (0 != PyModule_AddObject(module, typeName, PyTypeObjPtr))
                ythrow yexception() << "can't register type \"" << typeName << "\"";
        }

        void Register(PyObject* module, const char* objName, TObjectHolder* hld) {
            if (0 != PyModule_AddObject(module, objName, CreatePyObject(hld).RefGet()))
                ythrow yexception() << "can't register object \"" << objName << "\"";
        }

        void Register(TPyObjectPtr module, const TString& typeName) {
            Register(module.Get(), typeName.c_str());
        }

        void Register(TPyObjectPtr module, const TString& objName, TObjectHolder* hld) {
            Register(module.Get(), objName.c_str(), hld);
        }

        static TObjectHolder* CastToObjectHolder(PyObject* obj) {
            // Call Instance() to make sure PyTypeObjPtr is already created at this point
            Instance();
            if (!PyObject_IsInstance(obj, PyTypeObjPtr))
                return nullptr;
            TProxy* prx = reinterpret_cast<TProxy*>(obj);
            return prx ? prx->Holder : nullptr;
        }

        static TObject* CastToObject(PyObject* obj) {
            TObjectHolder* hld = CastToObjectHolder(obj);
            return hld ? TTraits::GetObject(*hld) : nullptr;
        }

        static TPyObjectPtr CreatePyObject(TObjectHolder* hld) {
            TPyObjectPtr r(_PyObject_New(&PyType), true);
            TProxy* prx = reinterpret_cast<TProxy*>(r.Get());
            if (prx)
                prx->Holder = hld;
            return r;
        }
    };

    template <typename TObjectHolder, typename TObject, typename TTraits>
    PyMappingMethods TPythonType<TObjectHolder, TObject, TTraits>::MappingMethods = {nullptr, nullptr, nullptr};

    template <typename TObjectHolder, typename TObject, typename TTraits>
    PyTypeObject TPythonType<TObjectHolder, TObject, TTraits>::PyType = {
        PyVarObject_HEAD_INIT(nullptr, 0) "", sizeof(TProxy), 0, (destructor)&DeallocObject
#if PY_VERSION_HEX < 0x030800b4
        , nullptr, /*tp_print*/
#endif
#if PY_VERSION_HEX >= 0x030800b4
        , 0, /*tp_vectorcall_offset*/
#endif
        &GetObjectAttr, &SetObjectAttr, nullptr, &GetRepr, nullptr, nullptr, &MappingMethods, nullptr, nullptr, &GetStr, nullptr, nullptr, nullptr,
        Py_TPFLAGS_DEFAULT, "", nullptr, nullptr, nullptr, 0, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, 0, InitObject, PyType_GenericAlloc, PyType_GenericNew, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, 0
#if PY_MAJOR_VERSION >= 3
        , nullptr
#endif
#if PY_VERSION_HEX >= 0x030800b1
        , nullptr /*tp_vectorcall*/
#endif
#if PY_VERSION_HEX >= 0x030800b4 && PY_VERSION_HEX < 0x03090000
        , nullptr /*tp_print*/
#endif
    };

    template <typename TObjectHolder, typename TObject, typename TTraits>
    PyObject* TPythonType<TObjectHolder, TObject, TTraits>::PyTypeObjPtr =
        reinterpret_cast<PyObject*>(&TPythonType<TObjectHolder, TObject, TTraits>::PyType);

    namespace NPrivate {
        template <typename TObject>
        class TUnboundClosureHolder {
        private:
            THolder<PyTypeObject> Holder;
            TString Method;

        public:
            TUnboundClosureHolder(PyTypeObject* ptr, const TString& meth)
                : Holder(ptr)
                , Method(meth)
            {
            }

            PyTypeObject* GetObject() const {
                return Holder.Get();
            }

            const TString GetMethod() const {
                return Method;
            }

            PyObject* Call(PyObject* obj, PyObject* args, PyObject*) const {
                TPyObjectPtr callable(PyObject_GetAttrString(obj, Method.c_str()), true);
                if (!callable.Get())
                    ythrow yexception() << "PyBind can't call method '" << Method << "'";
                TPyObjectPtr res(PyObject_CallObject(callable.Get(), args), true);
                if (!res.Get() && !PyErr_Occurred())
                    ythrow yexception() << "PyBind can't call method '" << Method << "'";
                return res.RefGet();
            }
        };

        template <typename TObject>
        class TUnboundClosure: public NPyBind::TPythonType<TUnboundClosureHolder<TObject>, PyTypeObject, TUnboundClosure<TObject>> {
        private:
            typedef class NPyBind::TPythonType<TUnboundClosureHolder<TObject>, PyTypeObject, TUnboundClosure<TObject>> TParent;
            friend class NPyBind::TPythonType<TUnboundClosureHolder<TObject>, PyTypeObject, TUnboundClosure<TObject>>;

            class TReprMethodCaller: public TBaseMethodCaller<PyTypeObject> {
            public:
                bool CallMethod(PyObject* closure, PyTypeObject*, PyObject*, PyObject*, PyObject*& res) const override {
                    TUnboundClosureHolder<TObject>* hld = TParent::CastToObjectHolder(closure);
                    TPyObjectPtr type((PyObject*)hld->GetObject());

                    TString nameStr;
                    TPyObjectPtr name(PyObject_GetAttrString(type.Get(), "__name__"), true);
                    if (!name.Get() || !FromPyObject(name.Get(), nameStr))
                        ythrow yexception() << "Could not get name of object";

                    TString methodName(hld->GetMethod());

                    TString message = "<unbound method " + nameStr + "." + methodName + ">";
                    res = ReturnString(message);
                    return (res != nullptr);
                }
            };

        private:
            TUnboundClosure()
                : TParent("", "")
            {
                TParent::AddCaller("__repr__", new TReprMethodCaller());
                TParent::AddCaller("__str__", new TReprMethodCaller());
                TParent::SetClosure(&Call);
            }

            static PyObject* Call(PyObject* closure, PyObject* args, PyObject* kwargs) {
                try {
                    TUnboundClosureHolder<TObject>* hld = TParent::CastToObjectHolder(closure);
                    if (!hld)
                        ythrow yexception() << "Can't cast object to TypeHolder";

                    size_t size = 0;
                    if (!PyTuple_Check(args) || (size = PyTuple_Size(args)) < 1)
                        ythrow yexception() << "Can't parse first argument: it should be valid object";
                    --size;
                    TPyObjectPtr obj(PyTuple_GetItem(args, 0));
                    TPyObjectPtr newArgs(PyTuple_New(size), true);

                    for (size_t i = 0; i < size; ++i) {
                        TPyObjectPtr item(PyTuple_GetItem(args, i + 1));
                        PyTuple_SetItem(newArgs.Get(), i, item.RefGet());
                    }

                    return hld->Call(obj.Get(), newArgs.Get(), kwargs);
                } catch (const std::exception& ex) {
                    PyErr_SetString(::NPyBind::TExceptionsHolder::Instance().ToPyException(ex).Get(), ex.what());
                } catch (...) {
                    PyErr_SetString(PyExc_RuntimeError, "Unknown error occurred while trying to call method");
                }
                return nullptr;
            }

            static PyTypeObject* GetObject(TUnboundClosureHolder<TObject>& obj) {
                return obj.GetObject();
            }
        };

        template <typename TObject>
        class TBoundClosureHolder {
        private:
            TPyObjectPtr Ptr;
            TObject* Object;
            TString Method;
            const TMethodCallers<TObject>& MethodCallers;

        public:
            TBoundClosureHolder(PyObject* ptr, TObject* obj, const TString& meth, const TMethodCallers<TObject>& callers)
                : Ptr(ptr)
                , Object(obj)
                , Method(meth)
                , MethodCallers(callers)
            {
            }

            TPyObjectPtr GetObjectPtr() const {
                return Ptr;
            }

            TObject* GetObject() const {
                return Object;
            }

            const TString GetMethod() const {
                return Method;
            }

            PyObject* Call(PyObject* args, PyObject* kwargs) const {
                PyObject* res = MethodCallers.CallMethod(Ptr.Get(), Object, args, kwargs, Method);
                if (res == nullptr && !PyErr_Occurred())
                    ythrow yexception() << "PyBind can't call method '" << Method << "'";
                return res;
            }
        };

        template <typename TObject>
        class TBoundClosure: public TPythonType<TBoundClosureHolder<TObject>, TObject, TBoundClosure<TObject>> {
        private:
            typedef TPythonType<TBoundClosureHolder<TObject>, TObject, TBoundClosure<TObject>> TMyParent;
            class TReprMethodCaller: public TBaseMethodCaller<TObject> {
            public:
                bool CallMethod(PyObject* closure, TObject*, PyObject*, PyObject*, PyObject*& res) const override {
                    TBoundClosureHolder<TObject>* hld = TMyParent::CastToObjectHolder(closure);
                    TPyObjectPtr obj(hld->GetObjectPtr());
                    TPyObjectPtr type(PyObject_Type(obj.Get()), true);

                    TString reprStr;
                    TPyObjectPtr repr(PyObject_Repr(obj.Get()), true);
                    if (!repr.Get() || !FromPyObject(repr.Get(), reprStr))
                        ythrow yexception() << "Could not get repr of object";

                    TString nameStr;
                    TPyObjectPtr name(PyObject_GetAttrString(type.Get(), "__name__"), true);
                    if (!name.Get() || !FromPyObject(name.Get(), nameStr))
                        ythrow yexception() << "Could not get name of object";

                    TString methodName(hld->GetMethod());

                    TString message = "<bound method " + nameStr + "." + methodName + " of " + reprStr + ">";
                    res = ReturnString(message);
                    return (res != nullptr);
                }
            };

        private:
            static PyObject* Call(PyObject* closure, PyObject* args, PyObject* kwargs) {
                try {
                    TBoundClosureHolder<TObject>* hld = TMyParent::CastToObjectHolder(closure);
                    if (!hld)
                        ythrow yexception() << "Can't cast object to ClosureHolder";

                    return hld->Call(args, kwargs);
                } catch (const std::exception& ex) {
                    PyErr_SetString(::NPyBind::TExceptionsHolder::Instance().ToPyException(ex).Get(), ex.what());
                } catch (...) {
                    PyErr_SetString(PyExc_RuntimeError, "Unknown error occurred while trying to call method");
                }
                return nullptr;
            }

        public:
            TBoundClosure()
                : TMyParent("", "")
            {
                TMyParent::AddCaller("__repr__", new TReprMethodCaller());
                TMyParent::AddCaller("__str__", new TReprMethodCaller());
                TMyParent::SetClosure(&Call);
            }

            static TObject* GetObject(const TBoundClosureHolder<TObject>& closure) {
                return closure.GetObject();
            }
        };

    }

    template <typename TObjectHolder, typename TObject, typename TTraits>
    PyObject* TPythonType<TObjectHolder, TObject, TTraits>::GetObjectAttr(PyObject* pyObj, char* attr) {
        try {
            TObject* obj = CastToObject(pyObj);
            PyObject* res = obj ? Instance().Attributes.GetAttrGetters().GetAttr(pyObj, *obj, attr) : nullptr;
            if (res == nullptr && Instance().Attributes.GetMethodCallers().HasMethod(pyObj, obj, attr)) {
                TPyObjectPtr r = NPrivate::TBoundClosure<TObject>::Instance().CreatePyObject(new NPrivate::TBoundClosureHolder<TObject>(pyObj, obj, attr, Instance().Attributes.GetMethodCallers()));
                res = r.RefGet();
            }
            if (res == nullptr && !PyErr_Occurred())
                ythrow TPyErr(PyExc_AttributeError) << "PyBind can't get attribute '" << attr << "'";
            return res;
        } catch (const std::exception& ex) {
            PyErr_SetString(TExceptionsHolder::Instance().ToPyException(ex).Get(), ex.what());
        } catch (...) {
            PyErr_SetString(PyExc_RuntimeError, (TString("Unknown error occurred while trying to get attribute '") + attr + "'").c_str());
        }
        return nullptr;
    }

    template <typename TObjectHolder, typename TObject, typename TTraits>
    int TPythonType<TObjectHolder, TObject, TTraits>::SetObjectAttr(PyObject* pyObj, char* attr, PyObject* value) {
        try {
            TObject* obj = CastToObject(pyObj);
            bool res = obj ? Instance().Attributes.GetAttrSetters().SetAttr(pyObj, *obj, attr, value) : false;
            if (!res && !PyErr_Occurred())
                ythrow yexception() << "PyBind can't set attribute '" << attr << "'";
            return res ? 0 : -1;
        } catch (const std::exception& ex) {
            PyErr_SetString(TExceptionsHolder::Instance().ToPyException(ex).Get(), ex.what());
        } catch (...) {
            PyErr_SetString(PyExc_RuntimeError, (TString("Unknown error occurred while trying to set attribute '") + attr + "'").c_str());
        }
        return -1;
    }

    template <typename TObjectHolder, typename TObject, typename TTraits>
    PyObject* TPythonType<TObjectHolder, TObject, TTraits>::GetStr(PyObject* obj) {
        try {
            TObject* self = CastToObject(obj);
            return Instance().Attributes.GetMethodCallers().CallMethod(obj, self, nullptr, nullptr, "__str__");
        } catch (const std::exception& ex) {
            PyErr_SetString(TExceptionsHolder::Instance().ToPyException(ex).Get(), ex.what());
        } catch (...) {
            PyErr_SetString(PyExc_RuntimeError, (TString("Unknown error occurred while trying to call '__str__'").c_str()));
        }
        return nullptr;
    }

    template <typename TObjectHolder, typename TObject, typename TTraits>
    PyObject* TPythonType<TObjectHolder, TObject, TTraits>::GetIter(PyObject* obj) {
        try {
            TObject* self = CastToObject(obj);
            return Instance().Attributes.GetMethodCallers().CallMethod(obj, self, nullptr, nullptr, "__iter__");
        } catch (const std::exception& ex) {
            PyErr_SetString(TExceptionsHolder::Instance().ToPyException(ex).Get(), ex.what());
        } catch (...) {
            PyErr_SetString(PyExc_RuntimeError, (TString("Unknown error occurred while trying to call '__iter__'").c_str()));
        }
        return nullptr;
    }

    template <typename TObjectHolder, typename TObject, typename TTraits>
    PyObject* TPythonType<TObjectHolder, TObject, TTraits>::GetNext(PyObject* obj) {
        try {
            TObject* self = CastToObject(obj);
            return Instance().Attributes.GetMethodCallers().CallMethod(obj, self, nullptr, nullptr, "next");
        } catch (const std::exception& ex) {
            PyErr_SetString(TExceptionsHolder::Instance().ToPyException(ex).Get(), ex.what());
        } catch (...) {
            PyErr_SetString(PyExc_RuntimeError, (TString("Unknown error occurred while trying to call 'next'").c_str()));
        }
        return nullptr;
    }

    template <typename TObjectHolder, typename TObject, typename TTraits>
    PyObject* TPythonType<TObjectHolder, TObject, TTraits>::GetRepr(PyObject* obj) {
        try {
            TObject* self = CastToObject(obj);
            return Instance().Attributes.GetMethodCallers().CallMethod(obj, self, nullptr, nullptr, "__repr__");
        } catch (const std::exception& ex) {
            PyErr_SetString(TExceptionsHolder::Instance().ToPyException(ex).Get(), ex.what());
        } catch (...) {
            PyErr_SetString(PyExc_RuntimeError, (TString("Unknown error occurred while trying to call '__repr__'").c_str()));
        }
        return nullptr;
    }
}