#pragma once

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <util/generic/strbuf.h>
#include <util/generic/vector.h>
#include <util/generic/set.h>
#include <util/generic/yexception.h>
#include <util/generic/hash.h>
#include <util/generic/map.h>
#include <util/generic/maybe.h>
#include <utility>
#include <initializer_list>
#include "ptr.h"

namespace NPyBind {
    PyObject* GetTrueRef(bool incref = true);
    PyObject* GetFalseRef(bool incref = true);

    PyObject* BuildPyObject(int val);
    PyObject* BuildPyObject(unsigned int val);
    PyObject* BuildPyObject(long int val);
    PyObject* BuildPyObject(unsigned long int val);
#ifdef PY_LONG_LONG
    PyObject* BuildPyObject(PY_LONG_LONG val);
    PyObject* BuildPyObject(unsigned PY_LONG_LONG val);
#endif
    PyObject* BuildPyObject(float val);
    PyObject* BuildPyObject(double val);
    PyObject* BuildPyObject(const TStringBuf& val);
    PyObject* BuildPyObject(const char* val);
    PyObject* BuildPyObject(const TWtringBuf& val);
    PyObject* BuildPyObject(const TBuffer& val);
    PyObject* BuildPyObject(bool val);
    PyObject* BuildPyObject(PyObject*);
    PyObject* BuildPyObject(TPyObjectPtr);

    template <typename T>
    PyObject* BuildPyObject(const TVector<T>& val);

    template <typename T>
    PyObject* BuildPyObject(const TSet<T>& val);

    template <typename TKey, typename TVal>
    PyObject* BuildPyObject(const THashMap<TKey, TVal>& val);

    template <typename T1, typename T2>
    PyObject* BuildPyObject(const std::pair<T1, T2>& val) {
        TPyObjectPtr first(BuildPyObject(val.first), true);
        if (!first) {
            return nullptr;
        }
        TPyObjectPtr second(BuildPyObject(val.second), true);
        if (!first || !second) {
            return nullptr;
        }
        TPyObjectPtr res(PyList_New(2), true);
        PyList_SetItem(res.Get(), 0, first.RefGet());
        PyList_SetItem(res.Get(), 1, second.RefGet());
        return res.RefGet();
    }

    template <typename T>
    PyObject* BuildPyObject(const TVector<T>& val) {
        TPyObjectPtr res(PyList_New(val.size()), true);
        for (size_t i = 0, size = val.size(); i < size; ++i) {
            auto pythonVal = BuildPyObject(std::move(val[i]));
            if (!pythonVal) {
                return nullptr;
            }
            PyList_SetItem(res.Get(), i, pythonVal);
        }
        return res.RefGet();
    }

    template <typename T>
    PyObject* BuildPyObject(TVector<T>&& val) {
        TPyObjectPtr res(PyList_New(val.size()), true);
        for (size_t i = 0, size = val.size(); i < size; ++i) {
            auto pythonVal = BuildPyObject(std::move(val[i]));
            if (!pythonVal) {
                return nullptr;
            }
            PyList_SetItem(res.Get(), i, pythonVal);
        }
        return res.RefGet();
    }

    template <typename T>
    PyObject* BuildPyObject(const TSet<T>& val) {
        TPyObjectPtr res(PySet_New(nullptr), true);
        for (const auto& v : val) {
            auto pythonVal = BuildPyObject(std::move(v));
            if (!pythonVal) {
                return nullptr;
            }
            PySet_Add(res.Get(), pythonVal);
        }
        return res.RefGet();
    }

    template <typename T>
    PyObject* BuildPyObject(const THashSet<T>& val) {
        TPyObjectPtr res(PySet_New(nullptr), true);
        for (const auto& v : val) {
            auto pythonVal = BuildPyObject(std::move(v));
            if (!pythonVal) {
                return nullptr;
            }
            PySet_Add(res.Get(), pythonVal);
        }
        return res.RefGet();
    }

    template <typename TKey, typename TVal>
    PyObject* BuildPyObject(const THashMap<TKey, TVal>& val) {
        TPyObjectPtr res(PyDict_New(), true);
        for (typename THashMap<TKey, TVal>::const_iterator it = val.begin(), end = val.end(); it != end; ++it) {
            auto prevOccurred = PyErr_Occurred();
            Y_UNUSED(prevOccurred);
            TPyObjectPtr k(BuildPyObject(it->first), true);
            if (!k) {
                return nullptr;
            }
            TPyObjectPtr v(BuildPyObject(it->second), true);
            if (!v) {
                return nullptr;
            }
            PyDict_SetItem(res.Get(), k.Get(), v.Get());
        }
        return res.RefGet();
    }

    template <typename TKey, typename TVal>
    PyObject* BuildPyObject(const TMap<TKey, TVal>& val) {
        TPyObjectPtr res(PyDict_New(), true);
        for (typename TMap<TKey, TVal>::const_iterator it = val.begin(), end = val.end(); it != end; ++it) {
            TPyObjectPtr k(BuildPyObject(it->first), true);
            if (!k) {
                return nullptr;
            }
            TPyObjectPtr v(BuildPyObject(it->second), true);
            if (!v) {
                return nullptr;
            }
            PyDict_SetItem(res.Get(), k.Get(), v.Get());
        }
        return res.RefGet();
    }


    template <typename TKey, typename TVal>
    PyObject* BuildPyObject(const TMultiMap<TKey, TVal>& val) {
        TPyObjectPtr res(PyDict_New(), true);
        TMaybe<TKey> prevKey;
        TPyObjectPtr currentEntry(PyList_New(0), true);
        for (const auto& [key, value]: val) {
            if (prevKey.Defined() && prevKey != key) {
                TPyObjectPtr pyPrevKey(BuildPyObject(*prevKey), true);
                if (!pyPrevKey) {
                    return nullptr;
                }
                PyDict_SetItem(res.Get(), pyPrevKey.Get(), currentEntry.Get());
                currentEntry = TPyObjectPtr(PyList_New(0), true);
            }
            TPyObjectPtr pyValue(BuildPyObject(value), true);
            if (!pyValue) {
                return nullptr;
            }
            PyList_Append(currentEntry.Get(), pyValue.Get());
            prevKey = key;
        }

        if (prevKey.Defined()) {
            TPyObjectPtr pyPrevKey(BuildPyObject(*prevKey), true);
            if (!pyPrevKey) {
                return nullptr;
            }
            PyDict_SetItem(res.Get(), pyPrevKey.Get(), currentEntry.Get());
        }
        return res.RefGet();
    }

    template <typename T>
    PyObject* BuildPyObject(const TMaybe<T>& val) {
        if (!val.Defined())
            Py_RETURN_NONE;
        return BuildPyObject(val.GetRef());
    }

    template <typename T, typename C, typename D>
    PyObject* BuildPyObject(const TSharedPtr<T, C, D>& val) {
        if (!val.Get())
            Py_RETURN_NONE;
        return BuildPyObject(*val.Get());
    }

    template <typename T>
    bool FromPyObject(PyObject* obj, T& res);

    bool FromPyObject(PyObject* obj, TString& res);
    bool FromPyObject(PyObject* obj, TStringBuf& res);
    bool FromPyObject(PyObject* obj, TUtf16String& res);
    bool FromPyObject(PyObject* obj, TBuffer& res);

    template <typename T>
    bool FromPyObject(PyObject* obj, TMaybe<T>& res) {
        //we need to save current error before trying derserialize the value
        //because it can produce conversion errors in python that we don't need to handle
        struct TError {
        public:
            TError() {
                PyErr_Fetch(&Type, &Value, &Traceback);
            }
            ~TError() {
                PyErr_Restore(Type, Value, Traceback);

            }
        private:
            PyObject* Type = nullptr;
            PyObject* Value = nullptr;
            PyObject* Traceback = nullptr;
        } currentPyExcInfo;
        T val;
        if (FromPyObject(obj, val)) {
            res = val;
            return true;
        }
        if (obj == Py_None) {
            res = Nothing();
            return true;
        }
        return false;
    }

    template <typename T1, typename T2>
    bool FromPyObject(PyObject* obj, std::pair<T1, T2>& res) {
        PyObject* first;
        PyObject* second;
        if (PyTuple_Check(obj) && 2 == PyTuple_Size(obj)) {
            first = PyTuple_GET_ITEM(obj, 0);
            second = PyTuple_GET_ITEM(obj, 1);
        } else if (PyList_Check(obj) && 2 == PyList_Size(obj)) {
            first = PyList_GET_ITEM(obj, 0);
            second = PyList_GET_ITEM(obj, 1);
        } else {
            return false;
        }
        return FromPyObject(first, res.first) && FromPyObject(second, res.second);
    }

    template <typename T>
    bool FromPyObject(PyObject* obj, TVector<T>& res) {
        if (!PyList_Check(obj))
            return false;
        size_t cnt = PyList_Size(obj);
        res.resize(cnt);
        for (size_t i = 0; i < cnt; ++i) {
            PyObject* item = PyList_GET_ITEM(obj, i);
            if (!FromPyObject(item, res[i]))
                return false;
        }
        return true;
    }

    template <typename K, typename V>
    bool FromPyObject(PyObject* obj, THashMap<K, V>& res) {
        if (!PyDict_Check(obj))
            return false;
        TPyObjectPtr list(PyDict_Keys(obj), true);
        size_t cnt = PyList_Size(list.Get());
        for (size_t i = 0; i < cnt; ++i) {
            PyObject* key = PyList_GET_ITEM(list.Get(), i);
            PyObject* value = PyDict_GetItem(obj, key);
            K rkey;
            V rvalue;
            if (!FromPyObject(key, rkey))
                return false;
            if (!FromPyObject(value, rvalue))
                return false;
            res[rkey] = rvalue;
        }
        return true;
    }

    template <typename K, typename V>
    bool FromPyObject(PyObject* obj, TMap<K, V>& res) {
        if (!PyDict_Check(obj))
            return false;
        TPyObjectPtr list(PyDict_Keys(obj), true);
        size_t cnt = PyList_Size(list.Get());
        for (size_t i = 0; i < cnt; ++i) {
            PyObject* key = PyList_GET_ITEM(list.Get(), i);
            PyObject* value = PyDict_GetItem(obj, key);
            K rkey;
            V rvalue;
            if (!FromPyObject(key, rkey))
                return false;
            if (!FromPyObject(value, rvalue))
                return false;
            res[rkey] = rvalue;
        }
        return true;
    }

    class cast_exception: public TBadCastException {
    };

    template <typename T>
    T FromPyObject(PyObject* obj) {
        T res;
        if (!FromPyObject(obj, res))
            ythrow cast_exception() << "Cannot cast argument to " << TypeName<T>();
        return res;
    }

    template <class... Args, std::size_t... I>
    bool ExtractArgs(std::index_sequence<I...>, PyObject* args, Args&... outArgs) {
        if (!args || !PyTuple_Check(args) || PyTuple_Size(args) != sizeof...(Args))
            return false;
        bool res = true;
        (void)std::initializer_list<bool>{(res = res && NPyBind::FromPyObject(PyTuple_GET_ITEM(args, I), outArgs))...};
        return res;
    }

    template <class... Args>
    bool ExtractArgs(PyObject* args, Args&... outArgs) {
        return ExtractArgs(std::index_sequence_for<Args...>(), args, outArgs...);
    }

    template <class... Args, std::size_t... I>
    bool ExtractOptionalArgs(std::index_sequence<I...>, PyObject* args, PyObject* kwargs, const char* keywords[], Args&... outArgs) {
        PyObject* pargs[sizeof...(Args)] = {};
        static const char format[sizeof...(Args) + 2] = {'|', ((void)I, 'O')..., 0};
        if (!PyArg_ParseTupleAndKeywords(args, kwargs, format, const_cast<char**>(keywords), &pargs[I]...))
            return false;
        bool res = true;
        (void)std::initializer_list<bool>{(res = res && (!pargs[I] || NPyBind::FromPyObject(pargs[I], outArgs)))...};
        return res;
    }

    template <class... Args>
    bool ExtractOptionalArgs(PyObject* args, PyObject* kwargs, const char* keywords[], Args&... outArgs) {
        return ExtractOptionalArgs(std::index_sequence_for<Args...>(), args, kwargs, keywords, outArgs...);
    }

    template <typename... Args, std::size_t... I>
    static auto GetArguments(std::index_sequence<I...>, PyObject* args) {
        Y_UNUSED(args); // gcc bug
        return std::make_tuple(FromPyObject<std::remove_cv_t<std::remove_reference_t<Args>>>(PyTuple_GetItem(args, I))...);
    }

    template <typename... Args>
    static auto GetArguments(PyObject* args) {
        return GetArguments<Args...>(std::index_sequence_for<Args...>(), args);
    }

    inline PyObject* ReturnString(TStringBuf s) {
#if PY_MAJOR_VERSION >= 3
        return PyUnicode_FromStringAndSize(s.data(), s.size());
#else
        return PyBytes_FromStringAndSize(s.data(), s.size());
#endif
    }

    inline TPyObjectPtr ReturnBytes(TStringBuf s) {
        return TPyObjectPtr(PyBytes_FromStringAndSize(s.data(), s.size()), true);
    }

    inline TPyObjectPtr NameFromString(TStringBuf s) {
        return TPyObjectPtr(ReturnString(s), true);
    }
}