/*
 * SPDX-FileCopyrightText: All Contributors to the PyTango project
 *
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

#pragma once

#include "from_py.h"
#include <iostream>

/**
 * Translation between python object to Tango data type.
 *
 * Example:
 * Tango::DevLong tg_value;
 * try
 * {
 *     from_py<Tango::DEV_LONG>::convert(py_obj, tg_value);
 * }
 * catch(bopy::error_already_set &eas)
 * {
 *     handle_error(eas);
 * }
 */
template <long tangoTypeConst>
struct from_py
{
    typedef typename TANGO_const2type(tangoTypeConst) TangoScalarType;

    static inline void convert(const bopy::object &o, TangoScalarType &tg)
    {
        convert(o.ptr(), tg);
    }

    static inline void convert(PyObject *o, TangoScalarType &tg)
    {
        // bopy::object tmp(bopy::handle<>(o));
        // tg = bopy::extract<TangoScalartype>(tmp);
        Tango::Except::throw_exception(
            "PyDs_WrongPythonDataTypeForAttribute", "Unsupported attribute type translation", "from_py::convert()");
    }
};

#define DEFINE_FAST_TANGO_FROMPY(tangoTypeConst, FN)                           \
    template <>                                                                \
    struct from_py<tangoTypeConst>                                             \
    {                                                                          \
        typedef TANGO_const2type(tangoTypeConst) TangoScalarType;              \
                                                                               \
        static inline void convert(const bopy::object &o, TangoScalarType &tg) \
        {                                                                      \
            convert(o.ptr(), tg);                                              \
        }                                                                      \
                                                                               \
        static inline void convert(PyObject *o, TangoScalarType &tg)           \
        {                                                                      \
            tg = static_cast<TangoScalarType>(FN(o));                          \
            if(PyErr_Occurred())                                               \
                bopy::throw_error_already_set();                               \
        }                                                                      \
    };

#undef max
#undef min

// DEFINE_FAST_TANGO_FROMPY should be enough. However, as python does not
// provide conversion from python integers to all the data types accepted
// by tango we must check the ranges manually. Also now we can add numpy
// support to some extent...
#define DEFINE_FAST_TANGO_FROMPY_NUM(tangoTypeConst, cpy_type, FN)                                           \
    template <>                                                                                              \
    struct from_py<tangoTypeConst>                                                                           \
    {                                                                                                        \
        typedef TANGO_const2type(tangoTypeConst) TangoScalarType;                                            \
        typedef std::numeric_limits<TangoScalarType> TangoScalarTypeLimits;                                  \
                                                                                                             \
        static inline void convert(const bopy::object &o, TangoScalarType &tg)                               \
        {                                                                                                    \
            convert(o.ptr(), tg);                                                                            \
        }                                                                                                    \
                                                                                                             \
        static inline void convert(PyObject *o, TangoScalarType &tg)                                         \
        {                                                                                                    \
            cpy_type cpy_value = FN(o);                                                                      \
            if(PyErr_Occurred())                                                                             \
            {                                                                                                \
                PyErr_Clear();                                                                               \
                if(PyArray_CheckScalar(o) &&                                                                 \
                   (PyArray_DescrFromScalar(o) == PyArray_DescrFromType(TANGO_const2numpy(tangoTypeConst)))) \
                {                                                                                            \
                    PyArray_ScalarAsCtype(o, reinterpret_cast<void *>(&tg));                                 \
                    return;                                                                                  \
                }                                                                                            \
                else                                                                                         \
                {                                                                                            \
                    std::string type_str;                                                                    \
                    if(tangoTypeConst == Tango::DEV_BOOLEAN)                                                 \
                    {                                                                                        \
                        type_str = "bool";                                                                   \
                    }                                                                                        \
                    else if(tangoTypeConst == Tango::DEV_FLOAT || tangoTypeConst == Tango::DEV_DOUBLE)       \
                    {                                                                                        \
                        type_str = "numeric";                                                                \
                    }                                                                                        \
                    else                                                                                     \
                    {                                                                                        \
                        type_str = "integer";                                                                \
                    };                                                                                       \
                    std::string err_msg = "Expecting a " + type_str +                                        \
                                          " type"                                                            \
                                          " but it is not. If you use a numpy type instead of"               \
                                          " python core types, then it must exactly match (ex:"              \
                                          " numpy.int32 for PyTango.DevLong)";                               \
                    PyErr_SetString(PyExc_TypeError, err_msg.c_str());                                       \
                    bopy::throw_error_already_set();                                                         \
                }                                                                                            \
            }                                                                                                \
            if(TangoScalarTypeLimits::is_integer)                                                            \
            {                                                                                                \
                if(cpy_value > (cpy_type) TangoScalarTypeLimits::max())                                      \
                {                                                                                            \
                    PyErr_SetString(PyExc_OverflowError, "Value is too large.");                             \
                    bopy::throw_error_already_set();                                                         \
                }                                                                                            \
                if(cpy_value < (cpy_type) TangoScalarTypeLimits::min())                                      \
                {                                                                                            \
                    PyErr_SetString(PyExc_OverflowError, "Value is too small.");                             \
                    bopy::throw_error_already_set();                                                         \
                }                                                                                            \
            }                                                                                                \
            tg = static_cast<TangoScalarType>(cpy_value);                                                    \
        }                                                                                                    \
    };

/* Allow for downcast */

inline unsigned PY_LONG_LONG PyLong_AsUnsignedLongLong_2(PyObject *pylong)
{
    unsigned PY_LONG_LONG result = PyLong_AsUnsignedLongLong(pylong);
    if(PyErr_Occurred())
    {
        PyErr_Clear();
        result = PyLong_AsUnsignedLong(pylong);
    }
    return result;
}

DEFINE_FAST_TANGO_FROMPY_NUM(Tango::DEV_BOOLEAN, long, PyLong_AsLong)
DEFINE_FAST_TANGO_FROMPY_NUM(Tango::DEV_UCHAR, unsigned long, PyLong_AsUnsignedLong)
DEFINE_FAST_TANGO_FROMPY_NUM(Tango::DEV_SHORT, long, PyLong_AsLong)
DEFINE_FAST_TANGO_FROMPY_NUM(Tango::DEV_USHORT, unsigned long, PyLong_AsUnsignedLong)
DEFINE_FAST_TANGO_FROMPY_NUM(Tango::DEV_LONG, long, PyLong_AsLong)
DEFINE_FAST_TANGO_FROMPY_NUM(Tango::DEV_ULONG, unsigned long, PyLong_AsUnsignedLong)
DEFINE_FAST_TANGO_FROMPY(Tango::DEV_STATE, PyLong_AsLong)

DEFINE_FAST_TANGO_FROMPY_NUM(Tango::DEV_LONG64, Tango::DevLong64, PyLong_AsLongLong)
DEFINE_FAST_TANGO_FROMPY_NUM(Tango::DEV_ULONG64, Tango::DevULong64, PyLong_AsUnsignedLongLong_2)
DEFINE_FAST_TANGO_FROMPY_NUM(Tango::DEV_FLOAT, double, PyFloat_AsDouble)
DEFINE_FAST_TANGO_FROMPY_NUM(Tango::DEV_DOUBLE, double, PyFloat_AsDouble)

DEFINE_FAST_TANGO_FROMPY(Tango::DEV_STRING, PyString_AsCorbaString)
DEFINE_FAST_TANGO_FROMPY(Tango::DEV_ENUM, PyLong_AsUnsignedLong)

template <long tangoArrayTypeConst>
struct array_element_from_py : public from_py<TANGO_const2scalarconst(tangoArrayTypeConst)>
{
};

template <>
struct array_element_from_py<Tango::DEVVAR_CHARARRAY>
{
    static const long tangoArrayTypeConst = Tango::DEVVAR_CHARARRAY;

    typedef TANGO_const2scalartype(tangoArrayTypeConst) TangoScalarType;
    typedef std::numeric_limits<TangoScalarType> TangoScalarTypeLimits;

    static inline void convert(const bopy::object &o, TangoScalarType &tg)
    {
        convert(o.ptr(), tg);
    }

    static inline void convert(PyObject *o, TangoScalarType &tg)
    {
        long cpy_value = PyLong_AsLong(o);
        if(PyErr_Occurred())
        {
            PyErr_Clear();
            if(PyArray_CheckScalar(o) &&
               (PyArray_DescrFromScalar(o) == PyArray_DescrFromType(TANGO_const2scalarnumpy(tangoArrayTypeConst))))
            {
                PyArray_ScalarAsCtype(o, reinterpret_cast<void *>(&tg));
                return;
            }
            else
            {
                std::string type_str;
                if(tangoArrayTypeConst == Tango::DEVVAR_BOOLEANARRAY)
                {
                    type_str = "bool";
                }
                else if(tangoArrayTypeConst == Tango::DEVVAR_DOUBLEARRAY ||
                        tangoArrayTypeConst == Tango::DEVVAR_FLOATARRAY)
                {
                    type_str = "numeric";
                }
                else
                {
                    type_str = "integer";
                }
                std::string err_msg = "Expecting a " + type_str +
                                      " type"
                                      " but it is not. If you use a numpy type instead of"
                                      " python core types, then it must exactly match (ex:"
                                      " numpy.int32 for PyTango.DevLong)";
                PyErr_SetString(PyExc_TypeError, err_msg.c_str());
                bopy::throw_error_already_set();
            }
        }
        if(TangoScalarTypeLimits::is_integer)
        {
            if(cpy_value > TangoScalarTypeLimits::max())
            {
                PyErr_SetString(PyExc_OverflowError, "Value is too large.");
                bopy::throw_error_already_set();
            }
            if(cpy_value < TangoScalarTypeLimits::min())
            {
                PyErr_SetString(PyExc_OverflowError, "Value is too small.");
                bopy::throw_error_already_set();
            }
        }
        tg = static_cast<TangoScalarType>(cpy_value);
    }
};

template <long tangoTypeConst>
inline void fast_python_to_tango_buffer_deleter__(typename TANGO_const2type(tangoTypeConst) * data_buffer,
                                                  long processedElements)
{
    delete[] data_buffer;
}

template <>
inline void fast_python_to_tango_buffer_deleter__<Tango::DEV_STRING>(Tango::DevString *data_buffer,
                                                                     long processedElements)
{
    for(long i = 0; i < processedElements; ++i)
    {
        delete[] data_buffer[i];
    }
    delete[] data_buffer;
}

template <long tangoTypeConst>
    inline typename TANGO_const2type(tangoTypeConst) * fast_python_to_tango_buffer_sequence(PyObject *py_val,
                                                                                            long *pdim_x,
                                                                                            long *pdim_y,
                                                                                            const std::string &fname,
                                                                                            bool isImage,
                                                                                            long &res_dim_x,
                                                                                            long &res_dim_y)
{
    typedef typename TANGO_const2type(tangoTypeConst) TangoScalarType;

    long dim_x;
    long dim_y = 0;
    Py_ssize_t len = PySequence_Size(py_val);
    bool expectFlatSource;

    if(isImage)
    {
        if(pdim_y)
        {
            expectFlatSource = true;
            dim_x = *pdim_x;
            dim_y = *pdim_y;
            long len2 = dim_x * dim_y;
            if(len2 < len)
            {
                len = len2;
            }
        }
        else
        {
            expectFlatSource = false;

            if(len > 0)
            {
                PyObject *py_row0 = PySequence_ITEM(py_val, 0);
                if(!py_row0 || !PySequence_Check(py_row0))
                {
                    Py_XDECREF(py_row0);
                    Tango::Except::throw_exception(
                        "PyDs_WrongParameters", "Expecting a sequence of sequences.", fname + "()");
                }

                dim_y = static_cast<long>(len);
                dim_x = static_cast<long>(PySequence_Size(py_row0));
                Py_XDECREF(py_row0);
            }
            else
            {
                dim_x = 0;
            }
        }
        len = dim_x * dim_y;
    }
    else
    {
        expectFlatSource = true;
        if(pdim_x)
        {
            if(*pdim_x > len)
            {
                Tango::Except::throw_exception(
                    "PyDs_WrongParameters", "Specified dim_x is larger than the sequence size", fname + "()");
            }
            len = *pdim_x;
        }
        if(pdim_y && (*pdim_y != 0))
        {
            Tango::Except::throw_exception(
                "PyDs_WrongParameters", "You should not specify dim_y for an spectrum attribute!", fname + "()");
        }
        dim_x = static_cast<long>(len);
    }

    res_dim_x = dim_x;
    res_dim_y = dim_y;

    if(!PySequence_Check(py_val))
    {
        Tango::Except::throw_exception("PyDs_WrongParameters", "Expecting a sequence!", fname + "()");
    }

    /// @bug Why not TangoArrayType::allocbuf(len)? Because
    /// I will use it in set_value(tg_ptr,...,release=true).
    /// Tango API makes delete[] tg_ptr instead of freebuf(tg_ptr).
    /// This is usually the same, but for Tango::DevStringArray the
    /// behaviour seems different and causes weirdtroubles..

    TangoScalarType *tg_ptr;
    tg_ptr = new TangoScalarType[len];

    // The boost extract could be used:
    // TangoScalarType val = bopy::extract<TangoScalarType>(elt_ptr);
    // instead of the code below.
    // the problem is that extract is considerably slower than our
    // convert function which only has to deal with the specific tango
    // data types

    PyObject *py_el = 0;
    PyObject *py_row = 0;
    TangoScalarType tg_scalar;
    long idx = 0;
    try
    {
        if(expectFlatSource)
        {
            for(idx = 0; idx < len; ++idx)
            {
                py_el = PySequence_ITEM(py_val, idx);
                if(!py_el)
                {
                    bopy::throw_error_already_set();
                }

                from_py<tangoTypeConst>::convert(py_el, tg_scalar);
                tg_ptr[idx] = tg_scalar;

                Py_DECREF(py_el);
                py_el = 0;
            }
        }
        else
        {
            for(long y = 0; y < dim_y; ++y)
            {
                py_row = PySequence_ITEM(py_val, y);
                if(!py_row)
                {
                    bopy::throw_error_already_set();
                }
                if(!PySequence_Check(py_row))
                {
                    Tango::Except::throw_exception(
                        "PyDs_WrongParameters", "Expecting a sequence of sequences!", fname + "()");
                }
                for(long x = 0; x < dim_x; ++x, ++idx)
                {
                    py_el = PySequence_ITEM(py_row, x);
                    if(!py_el)
                    {
                        bopy::throw_error_already_set();
                    }

                    from_py<tangoTypeConst>::convert(py_el, tg_scalar);
                    tg_ptr[x + y * dim_x] = tg_scalar;

                    Py_DECREF(py_el);
                    py_el = 0;
                }
                Py_DECREF(py_row);
                py_row = 0;
            }
        }
    }
    catch(...)
    {
        Py_XDECREF(py_el);
        Py_XDECREF(py_row);
        fast_python_to_tango_buffer_deleter__<tangoTypeConst>(tg_ptr, idx);
        throw;
    }
    return tg_ptr;
}

template <long tangoArrayTypeConst>
    inline typename TANGO_const2scalartype(tangoArrayTypeConst) *
    fast_python_to_corba_buffer_sequence(PyObject *py_val, long *pdim_x, const std::string &fname, long &res_dim_x)
{
    typedef typename TANGO_const2type(tangoArrayTypeConst) TangoArrayType;
    typedef typename TANGO_const2scalartype(tangoArrayTypeConst) TangoScalarType;

    long dim_x;
    Py_ssize_t len = PySequence_Size(py_val);

    if(pdim_x)
    {
        if(*pdim_x > len)
        {
            Tango::Except::throw_exception(
                "PyDs_WrongParameters", "Specified dim_x is larger than the sequence size", fname + "()");
        }
        len = *pdim_x;
    }
    dim_x = static_cast<long>(len);

    res_dim_x = dim_x;

    if(!PySequence_Check(py_val))
    {
        Tango::Except::throw_exception("PyDs_WrongParameters", "Expecting a sequence!", fname + "()");
    }

    TangoScalarType *tg_ptr = TangoArrayType::allocbuf(static_cast<Tango::DevULong>(len));

    // The boost extract could be used:
    // TangoScalarType val = bopy::extract<TangoScalarType>(elt_ptr);
    // instead of the code below.
    // the problem is that extract is considerably slower than our
    // convert function which only has to deal with the specific tango
    // data types

    PyObject *py_el = 0;
    TangoScalarType tg_scalar;
    long idx = 0;
    try
    {
        for(idx = 0; idx < len; ++idx)
        {
            py_el = PySequence_ITEM(py_val, idx);
            if(!py_el)
            {
                bopy::throw_error_already_set();
            }

            array_element_from_py<tangoArrayTypeConst>::convert(py_el, tg_scalar);
            tg_ptr[idx] = tg_scalar;

            Py_DECREF(py_el);
            py_el = 0;
        }
    }
    catch(...)
    {
        Py_XDECREF(py_el);
        TangoArrayType::freebuf(tg_ptr);
        throw;
    }
    return tg_ptr;
}

template <>
    inline TANGO_const2type(Tango::DEV_ENCODED) *
    fast_python_to_tango_buffer_sequence<Tango::DEV_ENCODED>(
        PyObject *, long *, long *, const std::string &fname, bool isImage, long &res_dim_x, long &res_dim_y)
{
    TangoSys_OMemStream o;
    o << "DevEncoded is only supported for SCALAR attributes." << std::ends;
    Tango::Except::throw_exception("PyDs_WrongPythonDataTypeForAttribute", o.str(), fname + "()");
    return 0;
}

#include "fast_from_py_numpy.hpp"

template <long tangoArrayTypeConst>
    inline typename TANGO_const2type(tangoArrayTypeConst) * fast_convert2array(bopy::object o)
{
    typedef typename TANGO_const2type(tangoArrayTypeConst) TangoArrayType;
    typedef typename TANGO_const2scalartype(tangoArrayTypeConst) TangoScalarType;

    long res_dim_x;

    // Last parameter false: destruction will be handled by CORBA, not by
    // Tango. So, when we destroy it manually later, we also have to use the
    // CORBA style (TangoArrayType are defines by CORBA idl)
    TangoScalarType *array =
        fast_python_to_corba_buffer_numpy<tangoArrayTypeConst>(o.ptr(), 0, "insert_array", res_dim_x);

    try
    {
        // not a bug: res_dim_y means nothing to us, we are unidimensional
        // here we have max_len and currebt_len = res_dim_x
        return new TangoArrayType(res_dim_x, res_dim_x, array, true);
    }
    catch(...)
    {
        TangoArrayType::freebuf(array);
        throw;
    }
    return 0;
}

template <>
    inline TANGO_const2type(Tango::DEVVAR_LONGSTRINGARRAY) *
    fast_convert2array<Tango::DEVVAR_LONGSTRINGARRAY>(bopy::object py_value)
{
    const long tangoArrayTypeConst = Tango::DEVVAR_LONGSTRINGARRAY;
    typedef TANGO_const2type(tangoArrayTypeConst) TangoArrayType;

    if(!PySequence_Check(py_value.ptr()))
    {
        raise_convert2array_DevVarLongStringArray();
    }

    size_t size = bopy::len(py_value);
    if(size != 2)
    {
        raise_convert2array_DevVarLongStringArray();
    }

    const bopy::object &py_lng = py_value[0], &py_str = py_value[1];

    unique_pointer<Tango::DevVarLongArray> a_lng(fast_convert2array<Tango::DEVVAR_LONGARRAY>(py_lng));

    unique_pointer<Tango::DevVarStringArray> a_str(fast_convert2array<Tango::DEVVAR_STRINGARRAY>(py_str));

    unique_pointer<TangoArrayType> result(new TangoArrayType());

    result->lvalue = *a_lng;
    result->svalue = *a_str;

    return result.release();
}

template <>
    inline TANGO_const2type(Tango::DEVVAR_DOUBLESTRINGARRAY) *
    fast_convert2array<Tango::DEVVAR_DOUBLESTRINGARRAY>(bopy::object py_value)
{
    const long tangoArrayTypeConst = Tango::DEVVAR_DOUBLESTRINGARRAY;
    typedef TANGO_const2type(tangoArrayTypeConst) TangoArrayType;

    if(!PySequence_Check(py_value.ptr()))
    {
        raise_convert2array_DevVarDoubleStringArray();
    }

    size_t size = bopy::len(py_value);
    if(size != 2)
    {
        raise_convert2array_DevVarDoubleStringArray();
    }

    const bopy::object &py_dbl = py_value[0], &py_str = py_value[1];

    unique_pointer<Tango::DevVarDoubleArray> a_dbl(fast_convert2array<Tango::DEVVAR_DOUBLEARRAY>(py_dbl));

    unique_pointer<Tango::DevVarStringArray> a_str(fast_convert2array<Tango::DEVVAR_STRINGARRAY>(py_str));

    unique_pointer<TangoArrayType> result(new TangoArrayType());

    result->dvalue = *a_dbl;
    result->svalue = *a_str;

    return result.release();
}
