#pragma once

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <util/generic/yexception.h>
#include <util/generic/map.h>
#include <util/generic/vector.h>
#include "ptr.h"

namespace NPyBind {
    // Usage:
    //   ythrow TPyErr(PyExc_TypeError) << "some python type error somewhere in your C++ code";
    //
    class TPyErr: public virtual yexception {
    public:
        TPyErr(PyObject* theException = PyExc_RuntimeError)
            : Exception(theException)
        {
        }

        TPyObjectPtr GetException() const {
            return Exception;
        }

    private:
        NPyBind::TPyObjectPtr Exception;
    };

    //Private api for creating PyBind python module
    //Needs only for overriding pybind python module in library which imports other pybind library
    namespace NPrivate {
        TPyObjectPtr CreatePyBindModule();
    }//NPrivate
    class TExceptionsHolder {
        friend TPyObjectPtr NPrivate::CreatePyBindModule();
    private:
        TExceptionsHolder(const TExceptionsHolder&);
        TExceptionsHolder& operator=(const TExceptionsHolder&);
        TExceptionsHolder();

        void Clear();
        TPyObjectPtr GetException(const TString&);
        TPyObjectPtr GetExceptions(const TVector<TString>&);
    private:
        class TExceptionsChecker {
        public:
            virtual ~TExceptionsChecker() {
            }
            virtual bool Check(const std::exception& ex) const = 0;
            virtual TString GetName() const = 0;
            virtual TPyObjectPtr GetException() const = 0;
        };

        template <typename TExcType>
        class TConcreteExceptionsChecker: public TExceptionsChecker {
        private:
            TString Name;
            TPyObjectPtr Exception;

        public:
            TConcreteExceptionsChecker(const TString& name, TPyObjectPtr exception)
                : Name(name)
                , Exception(exception)
            {
            }

            bool Check(const std::exception& ex) const override {
                const std::exception* e = &ex;
                return dynamic_cast<const TExcType*>(e);
            }

            TString GetName() const override {
                return Name;
            }

            TPyObjectPtr GetException() const override {
                return Exception;
            }
        };

        class TPyErrExceptionsChecker: public TExceptionsChecker {
        private:
            mutable TPyObjectPtr Exception;

        public:
            TPyErrExceptionsChecker() {
            }

            bool Check(const std::exception& ex) const override {
                const TPyErr* err = dynamic_cast<const TPyErr*>(&ex);
                if (err) {
                    Exception = err->GetException();
                }
                return err != nullptr;
            }

            TString GetName() const override {
                return TString();
            }

            TPyObjectPtr GetException() const override {
                return Exception;
            }
        };

        typedef TSimpleSharedPtr<TExceptionsChecker> TCheckerPtr;
        typedef TVector<TCheckerPtr> TCheckersVector;
        typedef TMap<TString, TPyObjectPtr> TExceptionsMap;

        TPyObjectPtr Module;
        TCheckersVector Checkers;
        TExceptionsMap Exceptions;

        static PyObject* DoInitPyBindModule();
        static void DoInitPyBindModule2();

    public:
        static TExceptionsHolder& Instance() {
            static TExceptionsHolder Holder;
            return Holder;
        }

        template <typename TExcType>
        void AddException(const TString& name, const TString& base = "") {
            TPyObjectPtr baseException(GetException(base));
            TString fullName = TString("pybind.") + name;
            TPyObjectPtr exception(PyErr_NewException(const_cast<char*>(fullName.c_str()), baseException.Get(), nullptr), true);
            Checkers.push_back(new TConcreteExceptionsChecker<TExcType>(name, exception));
            Exceptions[name] = exception;
        }

        template <typename TExcType>
        void AddException(const TString& name, const TVector<TString>& bases) {
            TPyObjectPtr baseExceptions(GetExceptions(bases));
            TString fullName = TString("pybind.") + name;
            TPyObjectPtr exception(PyErr_NewException(const_cast<char*>(fullName.c_str()), baseExceptions.Get(), nullptr), true);
            Checkers.push_back(new TConcreteExceptionsChecker<TExcType>(name, exception));
            Exceptions[name] = exception;
        }

        NPyBind::TPyObjectPtr ToPyException(const std::exception&);
    };
}
