#pragma once

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <util/generic/string.h>
#include <util/generic/map.h>
#include <util/generic/set.h>
#include <util/generic/vector.h>
#include <util/generic/ptr.h>

#include "cast.h"
#include "exceptions.h"

namespace NPyBind {
    // TBaseAttrGetter
    template <typename TObjType>
    class TBaseAttrGetter {
    public:
        virtual ~TBaseAttrGetter() {
        }
        virtual bool GetAttr(PyObject* owner, const TObjType& self, const TString& attr, PyObject*& res) const = 0;

        virtual bool HasAttr(PyObject* owner, const TObjType& self, const TString& attr, const TSet<TString>& hiddenNames) const {
            if (hiddenNames.find(attr) != hiddenNames.end())
                return false;
            PyObject* res = nullptr;
            if (!GetAttr(owner, self, attr, res))
                return false;
            Py_XDECREF(res);
            return true;
        }
    };

    template <typename TObjType>
    class TBaseAttrSetter {
    public:
        virtual ~TBaseAttrSetter() {
        }

        virtual bool SetAttr(PyObject* owner, TObjType& self, const TString& attr, PyObject* val) = 0;
    };

    template <typename TObjType>
    class TAttrGetters {
    public:
        typedef TSimpleSharedPtr<TBaseAttrGetter<TObjType>> TGetterPtr;

    private:
        typedef TVector<TGetterPtr> TGetterList;
        typedef TMap<TString, TGetterList> TGetterMap;

        const TSet<TString>& HiddenAttrNames;
        TGetterMap Getters;

    public:
        TAttrGetters(const TSet<TString>& hiddenNames)
            : HiddenAttrNames(hiddenNames)
        {
        }

        void AddGetter(const TString& attr, TGetterPtr getter) {
            Getters[attr].push_back(getter);
        }

        PyObject* GetAttr(PyObject* owner, const TObjType& self, const TString& attr) const {
            typename TGetterMap::const_iterator it1 = Getters.find(attr);
            if (it1 == Getters.end())
                it1 = Getters.find("");
            if (it1 == Getters.end())
                return nullptr;
            const TGetterList& lst = it1->second;
            for (typename TGetterList::const_iterator it2 = lst.begin(), end = lst.end(); it2 != end; ++it2) {
                PyObject* res = nullptr;
                if ((*it2)->GetAttr(owner, self, attr, res))
                    return res;
                // IMPORTANT!
                // we have to fail GetAttr right there  because we've failed because of internal python error/exception and can't continue iterating because
                // it cause subsequent exceptions during call to Py_BuildValue
                // moreover we have to preserve original exception right there
                if (PyErr_Occurred()) {
                    break;
                }
            }
            return nullptr;
        }

        bool HasAttr(PyObject* owner, const TObjType& self, const TString& attr) const {
            typename TGetterMap::const_iterator it1 = Getters.find(attr);
            if (it1 == Getters.end())
                return false;
            const TGetterList& lst = it1->second;
            for (typename TGetterList::const_iterator it2 = lst.begin(), end = lst.end(); it2 != end; ++it2) {
                if ((*it2)->HasAttr(owner, self, attr, HiddenAttrNames))
                    return true;
            }
            return false;
        }

        void GetAttrsDictionary(PyObject* owner, const TObjType& self, TMap<TString, PyObject*>& res) const {
            for (typename TGetterMap::const_iterator it = Getters.begin(), end = Getters.end(); it != end; ++it) {
                try {
                    if (HasAttr(owner, self, it->first)) {
                        auto attrPtr = GetAttr(owner, self, it->first);
                        if (attrPtr) {
                            res[it->first] = attrPtr;
                        }
                        if (PyErr_Occurred()) {
                            PyErr_Clear(); // Skip python errors as well
                        }
                    }
                } catch (const std::exception&) {
                    // ignore this field
                }
            }
        }

        void GetAttrsNames(PyObject* owner, const TObjType& self, TVector<TString>& resultNames) const {
            for (typename TGetterMap::const_iterator it = Getters.begin(), end = Getters.end(); it != end; ++it) {
                if (HasAttr(owner, self, it->first))
                    resultNames.push_back(it->first);
            }
        }
    };

    template <typename TObjType>
    class TGenericAttrGetter: public TBaseAttrGetter<TObjType> {
    private:
        TString AttrName;

    public:
        TGenericAttrGetter(const TString& attrName)
            : AttrName(attrName)
        {
        }

        bool GetAttr(PyObject* obj, const TObjType&, const TString&, PyObject*& res) const override {
            auto str = NameFromString(AttrName);
            res = PyObject_GenericGetAttr(obj, str.Get());
            if (!res && !PyErr_Occurred())
                ythrow TPyErr(PyExc_AttributeError) << "Can't get generic attribute '" << AttrName << "'";
            return res;
        }
    };

    template <typename TObjType>
    class TAttrSetters {
    private:
        typedef TSimpleSharedPtr<TBaseAttrSetter<TObjType>> TSetterPtr;
        typedef TVector<TSetterPtr> TSetterList;
        typedef TMap<TString, TSetterList> TSetterMap;

        TSetterMap Setters;

    public:
        void AddSetter(const TString& attr, TSetterPtr setter) {
            Setters[attr].push_back(setter);
        }

        bool SetAttr(PyObject* owner, TObjType& self, const TString& attr, PyObject* val) {
            typename TSetterMap::const_iterator it1 = Setters.find(attr);
            if (it1 == Setters.end())
                it1 = Setters.find("");
            if (it1 == Setters.end())
                return false;
            const TSetterList& lst = it1->second;
            for (typename TSetterList::const_iterator it2 = lst.begin(), end = lst.end(); it2 != end; ++it2) {
                if ((*it2)->SetAttr(owner, self, attr, val))
                    return true;
            }
            return false;
        }

        bool SetAttrDictionary(PyObject* owner, TObjType& self, TMap<TString, PyObject*>& dict) {
            for (TMap<TString, PyObject*>::const_iterator it = dict.begin(), end = dict.end(); it != end; ++it) {
                try {
                    SetAttr(owner, self, it->first, it->second);
                } catch (std::exception&) {
                    // ignore this field
                }
            }

            return true;
        }
    };

    /**
      * TMethodAttrGetter - this class maps Python attribute read to C++ method call
      */
    template <typename TObjType, typename TResult, typename TSubObject>
    class TMethodAttrGetter: public TBaseAttrGetter<TObjType> {
    private:
        typedef TResult (TSubObject::*TMethod)() const;
        TMethod Method;

    public:
        TMethodAttrGetter(TMethod method)
            : Method(method)
        {
        }

        bool GetAttr(PyObject*, const TObjType& self, const TString&, PyObject*& res) const override {
            const TSubObject* sub = dynamic_cast<const TSubObject*>(&self);
            if (sub == nullptr)
                return false;
            res = BuildPyObject((sub->*Method)());
            return (res != nullptr);
        }
    };

    template <typename TObjType, typename TFunctor>
    class TFunctorAttrGetter: public TBaseAttrGetter<TObjType> {
        TFunctor Functor;
    public:
        explicit TFunctorAttrGetter(TFunctor functor)
            : Functor(functor)
        {
        }

        bool GetAttr(PyObject*, const TObjType& self, const TString&, PyObject*& res) const override {
            res = BuildPyObject(Functor(self));
            return (res != nullptr);
        }
    };


    /**
      * TMethodAttrGetterWithCheck - this class maps Python attribute read to C++ HasAttr/GetAttr call
      *     If HasAttr returns false, None is returned.
      *     Otherwise GetAttr is called.
      */
    template <typename TObjType, typename TResult, typename TSubObject>
    class TMethodAttrGetterWithCheck: public TBaseAttrGetter<TObjType> {
    private:
        typedef TResult (TSubObject::*TMethod)() const;
        typedef bool (TSubObject::*TCheckerMethod)() const;
        TMethod Method;
        TCheckerMethod CheckerMethod;

    public:
        TMethodAttrGetterWithCheck(TMethod method, TCheckerMethod checkerMethod)
            : Method(method)
            , CheckerMethod(checkerMethod)
        {
        }

        bool GetAttr(PyObject*, const TObjType& self, const TString&, PyObject*& res) const override {
            const TSubObject* sub = dynamic_cast<const TSubObject*>(&self);
            if (sub == nullptr)
                return false;
            if ((sub->*CheckerMethod)())
                res = BuildPyObject((sub->*Method)());
            else {
                Py_INCREF(Py_None);
                res = Py_None;
            }
            return (res != nullptr);
        }
    };

    template <typename TObjType, typename TResult, typename TSubObject, typename TMapper>
    class TMethodAttrMappingGetter: public TBaseAttrGetter<TObjType> {
    private:
        typedef TResult (TSubObject::*TMethod)() const;

        TMethod Method;
        TMapper Mapper;

    public:
        TMethodAttrMappingGetter(TMethod method, TMapper mapper)
            : Method(method)
            , Mapper(mapper)
        {
        }

        bool GetAttr(PyObject*, const TObjType& self, const TString&, PyObject*& res) const override {
            const TSubObject* sub = dynamic_cast<const TSubObject*>(&self);
            if (sub == nullptr)
                return false;
            res = BuildPyObject(Mapper((sub->*Method)()));
            return (res != nullptr);
        }
    };

    template <typename TObjType, typename TResult, typename TSubObject, typename TMapper>
    TSimpleSharedPtr<TBaseAttrGetter<TObjType>>
    CreateMethodAttrMappingGetter(TResult (TSubObject::*method)() const,
                                  TMapper mapper) {
        return new TMethodAttrMappingGetter<TObjType, TResult, TSubObject, TMapper>(method,
                                                                                    mapper);
    }

    template <typename TObjType, typename TResult, typename TValue, typename TSubObject>
    class TMethodAttrSetter: public TBaseAttrSetter<TObjType> {
    private:
        typedef TResult (TSubObject::*TMethod)(TValue&);
        TMethod Method;

    public:
        TMethodAttrSetter(TMethod method)
            : Method(method)
        {
        }

        virtual bool SetAttr(PyObject*, TObjType& self, const TString&, PyObject* val) {
            TSubObject* sub = dynamic_cast<TSubObject*>(&self);
            if (sub == nullptr)
                return false;
            TValue value;
            if (!FromPyObject(val, value))
                return false;
            (sub->*Method)(value);
            return true;
        }
    };

    template <typename TObjType, typename TValue, typename TFunctor>
    class TFunctorAttrSetter: public TBaseAttrSetter<TObjType> {
        TFunctor Functor;
    public:
        explicit TFunctorAttrSetter(TFunctor functor)
            : Functor(functor)
        {
        }

        bool SetAttr(PyObject*, TObjType& self, const TString&, PyObject* val) const override {
            TValue value;
            if (!FromPyObject(val, value))
                return false;
            auto res = BuildPyObject(Functor(self, value));
            return (res != nullptr);
        }
    };
    template <typename TObjType, typename TResult, typename TSubObject>
    TSimpleSharedPtr<TBaseAttrGetter<TObjType>> CreateMethodAttrGetter(TResult (TSubObject::*method)() const) {
        return new TMethodAttrGetter<TObjType, TResult, TSubObject>(method);
    }

    template <typename TObjType, typename TFunctor>
    TSimpleSharedPtr<TFunctorAttrGetter<TObjType, TFunctor>> CreateFunctorAttrGetter(TFunctor functor) {
        return MakeSimpleShared<TFunctorAttrGetter<TObjType, TFunctor>>(functor);
    }

    template <typename TObjType, typename TResult, typename TSubObject>
    TSimpleSharedPtr<TBaseAttrGetter<TObjType>> CreateMethodAttrGetterWithCheck(
        TResult (TSubObject::*method)() const,
        bool (TSubObject::*checkerMethod)() const) {
        return new TMethodAttrGetterWithCheck<TObjType, TResult, TSubObject>(method, checkerMethod);
    }

    template <typename TObjType, typename TResult, typename TValue, typename TSubObject>
    TSimpleSharedPtr<TBaseAttrSetter<TObjType>> CreateMethodAttrSetter(TResult (TSubObject::*method)(TValue&)) {
        return new TMethodAttrSetter<TObjType, TResult, TValue, TSubObject>(method);
    }

    template <typename TObjType, typename TFunctor, typename TValue>
    TSimpleSharedPtr<TFunctorAttrSetter<TObjType, TValue, TFunctor>> CreateFunctorAttrSetter(TFunctor functor) {
        return MakeSimpleShared<TFunctorAttrSetter<TObjType, TValue, TFunctor>>(functor);
    }

    template <typename TObjType, typename TValue, typename TSubObject>
    class TDirectAttrSetter: public TBaseAttrSetter<TObjType> {
    private:
        typedef TValue TSubObject::*TValueType;
        TValueType Value;

    public:
        TDirectAttrSetter(TValueType value)
            : Value(value)
        {
        }

        bool SetAttr(PyObject*, TObjType& self, const TString&, PyObject* val) override {
            TSubObject* sub = dynamic_cast<TSubObject*>(&self);
            if (sub == NULL)
                return false;
            if (!FromPyObject(val, sub->*Value))
                return false;
            return true;
        }
    };

    template <typename TObjType, typename TValue, typename TSubObject>
    TSimpleSharedPtr<TBaseAttrSetter<TObjType>> CreateAttrSetter(TValue TSubObject::*value) {
        return new TDirectAttrSetter<TObjType, TValue, TSubObject>(value);
    }

    template <typename TObjType, typename TValue, typename TSubObject>
    class TDirectAttrGetter: public TBaseAttrGetter<TObjType> {
    private:
        typedef TValue TSubObject::*TValueType;
        TValueType Value;

    public:
        TDirectAttrGetter(TValueType value)
            : Value(value)
        {
        }

        bool GetAttr(PyObject*, const TObjType& self, const TString&, PyObject*& res) const override {
            const TSubObject* sub = dynamic_cast<const TSubObject*>(&self);
            if (sub == nullptr)
                return false;
            res = BuildPyObject(sub->*Value);
            return (res != nullptr);
        }
    };

    template <typename TObjType, typename TValue, typename TSubObject>
    TSimpleSharedPtr<TBaseAttrGetter<TObjType>> CreateAttrGetter(TValue TSubObject::*value) {
        return new TDirectAttrGetter<TObjType, TValue, TSubObject>(value);
    }
}