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

#include "precompiled_header.hpp"
#include "defs.h"
#include "pytgutils.h"
#include "pipe.h"
#include "fast_from_py.h"
#include <boost/python.hpp>
#include "device_pipe.h"

#define __AUX_DECL_CALL_PIPE_METHOD                                      \
    PyDeviceImplBase *__dev_ptr = dynamic_cast<PyDeviceImplBase *>(dev); \
    AutoPythonGIL __py_lock;

#define __AUX_CATCH_PY_EXCEPTION         \
    catch(bopy::error_already_set & eas) \
    {                                    \
        handle_python_exception(eas);    \
    }

#define CALL_PIPE_METHOD(dev, name)                         \
    __AUX_DECL_CALL_PIPE_METHOD                             \
    try                                                     \
    {                                                       \
        bopy::call_method<void>(__dev_ptr->the_self, name); \
    }                                                       \
    __AUX_CATCH_PY_EXCEPTION

#define CALL_PIPE_METHOD_VARGS(dev, name, ...)                           \
    __AUX_DECL_CALL_PIPE_METHOD                                          \
    try                                                                  \
    {                                                                    \
        bopy::call_method<void>(__dev_ptr->the_self, name, __VA_ARGS__); \
    }                                                                    \
    __AUX_CATCH_PY_EXCEPTION

#define CALL_PIPE_METHOD_RET(retType, ret, dev, name)                \
    __AUX_DECL_CALL_PIPE_METHOD                                      \
    try                                                              \
    {                                                                \
        ret = bopy::call_method<retType>(__dev_ptr->the_self, name); \
    }                                                                \
    __AUX_CATCH_PY_EXCEPTION

#define CALL_PIPE_METHOD_VARGS_RET(retType, ret, dev, name, ...)                  \
    __AUX_DECL_CALL_PIPE_METHOD                                                   \
    try                                                                           \
    {                                                                             \
        ret = bopy::call_method<retType>(__dev_ptr->the_self, name, __VA_ARGS__); \
    }                                                                             \
    __AUX_CATCH_PY_EXCEPTION

#define RET_CALL_PIPE_METHOD(retType, dev, name)                      \
    __AUX_DECL_CALL_PIPE_METHOD                                       \
    try                                                               \
    {                                                                 \
        return bopy::call_method<retType>(__dev_ptr->the_self, name); \
    }                                                                 \
    __AUX_CATCH_PY_EXCEPTION

#define RET_CALL_PIPE_METHOD_VARGS(retType, dev, name, ...)                        \
    __AUX_DECL_CALL_PIPE_METHOD                                                    \
    try                                                                            \
    {                                                                              \
        return bopy::call_method<retType>(__dev_ptr->the_self, name, __VA_ARGS__); \
    }                                                                              \
    __AUX_CATCH_PY_EXCEPTION

namespace PyTango
{
namespace Pipe
{

void _Pipe::read(Tango::DeviceImpl *dev, Tango::Pipe &pipe)
{
    if(!_is_method(dev, read_name))
    {
        TangoSys_OMemStream o;
        o << read_name << " method "
          << " not found for " << pipe.get_name();
        Tango::Except::throw_exception("PyTango_ReadPipeMethodNotFound", o.str(), "PyTango::Pipe::read");
    }

    CALL_PIPE_METHOD_VARGS(dev, read_name.c_str(), boost::ref(pipe))
}

void _Pipe::write(Tango::DeviceImpl *dev, Tango::WPipe &pipe)
{
    if(!_is_method(dev, write_name))
    {
        TangoSys_OMemStream o;
        o << write_name << " method not found for " << pipe.get_name();
        Tango::Except::throw_exception("PyTango_WritePipeMethodNotFound", o.str(), "PyTango::Pipe::write");
    }
    PyDeviceImplBase *__dev_ptr = dynamic_cast<PyDeviceImplBase *>(dev);
    AutoPythonGIL __py_lock;
    try
    {
        bopy::call_method<bopy::object>(__dev_ptr->the_self, write_name.c_str(), boost::ref(pipe));
    }
    catch(bopy::error_already_set &eas)
    {
        handle_python_exception(eas);
    }
}

bool _Pipe::is_allowed(Tango::DeviceImpl *dev, Tango::PipeReqType ty)
{
    if(_is_method(dev, py_allowed_name))
    {
        RET_CALL_PIPE_METHOD_VARGS(bool, dev, py_allowed_name.c_str(), ty)
    }
    // keep compiler quiet
    return true;
}

bool _Pipe::_is_method(Tango::DeviceImpl *dev, const std::string &name)
{
    AutoPythonGIL __py_lock;
    PyDeviceImplBase *__dev_ptr = dynamic_cast<PyDeviceImplBase *>(dev);
    PyObject *__dev_py = __dev_ptr->the_self;
    return is_method_defined(__dev_py, name);
}

static void throw_wrong_python_data_type(const std::string &name, const char *method)
{
    TangoSys_OMemStream o;
    o << "Wrong Python type for pipe " << name << std::ends;
    Tango::Except::throw_exception("PyDs_WrongPythonDataTypeForPipe", o.str(), method);
}

template <typename T>
void append_scalar_encoded(T &obj, const std::string &name, bopy::object &py_value)
{
    bopy::object p0 = py_value[0];
    bopy::object p1 = py_value[1];

    const char *encoded_format = bopy::extract<const char *>(p0.ptr());

    PyObject *data_ptr = p1.ptr();
    Py_buffer view;

    if(PyObject_GetBuffer(data_ptr, &view, PyBUF_FULL_RO) < 0)
    {
        throw_wrong_python_data_type(obj.get_name(), "append_scalar_encoded");
    }

    CORBA::ULong nb = static_cast<CORBA::ULong>(view.len);
    Tango::DevVarCharArray arr(nb, nb, (CORBA::Octet *) view.buf, false);
    Tango::DevEncoded value;
    value.encoded_format = CORBA::string_dup(encoded_format);
    value.encoded_data = arr;
    obj << value;
    PyBuffer_Release(&view);
}

template <typename T, long tangoTypeConst>
void __append_scalar(T &obj, const std::string &name, bopy::object &py_value)
{
    typedef typename TANGO_const2type(tangoTypeConst) TangoScalarType;
    TangoScalarType tg_value;
    from_py<tangoTypeConst>::convert(py_value, tg_value);
    Tango::DataElement<TangoScalarType> data_elt(name, tg_value);
    obj << data_elt;
}

template <long tangoTypeConst>
void append_scalar(Tango::Pipe &pipe, const std::string &name, bopy::object &py_value)
{
    __append_scalar<Tango::Pipe, tangoTypeConst>(pipe, name, py_value);
}

template <>
void append_scalar<Tango::DEV_VOID>(Tango::Pipe &pipe, const std::string &name, bopy::object &py_value)
{
    throw_wrong_python_data_type(pipe.get_name(), "append_scalar");
}

template <>
void append_scalar<Tango::DEV_PIPE_BLOB>(Tango::Pipe &pipe, const std::string &name, bopy::object &py_value)
{
    throw_wrong_python_data_type(pipe.get_name(), "append_scalar");
}

template <>
void append_scalar<Tango::DEV_ENCODED>(Tango::Pipe &pipe, const std::string &name, bopy::object &py_value)
{
    append_scalar_encoded<Tango::Pipe>(pipe, name, py_value);
}

template <long tangoTypeConst>
void append_scalar(Tango::DevicePipeBlob &blob, const std::string &name, bopy::object &py_value)
{
    __append_scalar<Tango::DevicePipeBlob, tangoTypeConst>(blob, name, py_value);
}

template <>
void append_scalar<Tango::DEV_VOID>(Tango::DevicePipeBlob &blob, const std::string &name, bopy::object &py_value)
{
    throw_wrong_python_data_type(blob.get_name(), "append_scalar");
}

template <>
void append_scalar<Tango::DEV_PIPE_BLOB>(Tango::DevicePipeBlob &blob, const std::string &name, bopy::object &py_value)
{
    throw_wrong_python_data_type(blob.get_name(), "append_scalar");
}

template <>
void append_scalar<Tango::DEV_ENCODED>(Tango::DevicePipeBlob &blob, const std::string &name, bopy::object &py_value)
{
    append_scalar_encoded<Tango::DevicePipeBlob>(blob, name, py_value);
}

// -------------
// Array version
// -------------

template <typename T, long tangoArrayTypeConst>
void __append_array(T &obj, const std::string &name, bopy::object &py_value)
{
    typedef typename TANGO_const2type(tangoArrayTypeConst) TangoArrayType;

    TangoArrayType *value = fast_convert2array<tangoArrayTypeConst>(py_value);
    obj << value;
}

template <long tangoArrayTypeConst>
void append_array(Tango::Pipe &pipe, const std::string &name, bopy::object &py_value)
{
    __append_array<Tango::Pipe, tangoArrayTypeConst>(pipe, name, py_value);
}

template <>
void append_array<Tango::DEV_VOID>(Tango::Pipe &pipe, const std::string &name, bopy::object &py_value)
{
    throw_wrong_python_data_type(pipe.get_name(), "append_array");
}

template <>
void append_array<Tango::DEV_PIPE_BLOB>(Tango::Pipe &pipe, const std::string &name, bopy::object &py_value)
{
    throw_wrong_python_data_type(pipe.get_name(), "append_array");
}

template <>
void append_array<Tango::DEVVAR_LONGSTRINGARRAY>(Tango::Pipe &pipe, const std::string &name, bopy::object &py_value)
{
    throw_wrong_python_data_type(pipe.get_name(), "append_array");
}

template <>
void append_array<Tango::DEVVAR_DOUBLESTRINGARRAY>(Tango::Pipe &pipe, const std::string &name, bopy::object &py_value)
{
    throw_wrong_python_data_type(pipe.get_name(), "append_array");
}

template <long tangoArrayTypeConst>
void append_array(Tango::DevicePipeBlob &blob, const std::string &name, bopy::object &py_value)
{
    __append_array<Tango::DevicePipeBlob, tangoArrayTypeConst>(blob, name, py_value);
}

template <>
void append_array<Tango::DEV_VOID>(Tango::DevicePipeBlob &blob, const std::string &name, bopy::object &py_value)
{
    throw_wrong_python_data_type(blob.get_name(), "append_array");
}

template <>
void append_array<Tango::DEV_PIPE_BLOB>(Tango::DevicePipeBlob &blob, const std::string &name, bopy::object &py_value)
{
    throw_wrong_python_data_type(blob.get_name(), "append_array");
}

template <>
void append_array<Tango::DEVVAR_LONGSTRINGARRAY>(Tango::DevicePipeBlob &blob,
                                                 const std::string &name,
                                                 bopy::object &py_value)
{
    throw_wrong_python_data_type(blob.get_name(), "append_array");
}

template <>
void append_array<Tango::DEVVAR_DOUBLESTRINGARRAY>(Tango::DevicePipeBlob &blob,
                                                   const std::string &name,
                                                   bopy::object &py_value)
{
    throw_wrong_python_data_type(blob.get_name(), "append_array");
}

template <typename T>
void __append(T &obj, const std::string &name, bopy::object &py_value, const Tango::CmdArgType dtype)
{
    TANGO_DO_ON_DEVICE_DATA_TYPE_ID(dtype, append_scalar<tangoTypeConst>(obj, name, py_value);
                                    , append_array<tangoTypeConst>(obj, name, py_value););
}

/*
template<typename T>
void __set_value(T& obj, bopy::object& py_value)
{
    bopy::object items = py_value.attr("items")();
    Py_ssize_t size = bopy::len(items);
    obj.set_data_elt_nb(size);
    for(size_t i = 0; i < size; ++i)
    {
        std::string item_name = bopy::extract<std::string>(items[i][0]);
        bopy::object py_item_data = items[i][1];
        if (PyDict_Check(py_item_data.ptr()))  // data element
        {
            bopy::object py_item_value = py_item_data["value"];
            bopy::object py_item_dtype = py_item_data["dtype"];
            Tango::CmdArgType item_dtype = bopy::extract<Tango::CmdArgType>(py_item_dtype);
            __append(obj, item_name, py_item_value, item_dtype);
        }
        else
        {
            std::string blob_name = bopy::extract<std::string>(py_item_data[0]);
            bopy::object py_blob_data = py_item_data[1];
            Tango::DevicePipeBlob blob(blob_name);
            __set_value(blob, py_blob_data);
            obj << blob;
        }
    }
}
*/
/*
template<typename T>
void __set_value(T& obj, bopy::object& py_value)
{
    bopy::str name_key("name");
    Py_ssize_t size = bopy::len(py_value);
    std::vector<std::string> elem_names;
    for(size_t i = 0; i < size; ++i)
    {
        elem_names.push_back(bopy::extract<std::string>(py_value[i][0]));
    }
    obj.set_data_elt_names(elem_names);

    for(size_t i = 0; i < size; ++i)
    {
        std::string item_name = bopy::extract<std::string>(py_value[i][0]);
        bopy::dict py_item_data = bopy::extract<bopy::dict>(py_value[i][1]);
        if (py_item_data.has_key(name_key)) // a sub-blob
        {
            std::string blob_name = bopy::extract<std::string>(py_item_data["name"]);
            bopy::object py_blob_data = py_item_data["data"];
            Tango::DevicePipeBlob blob(blob_name);
            __set_value(blob, py_blob_data);
            obj << blob;
        }
        else
        {
            bopy::object py_item_value = py_item_data["value"];
            bopy::object py_item_dtype = py_item_data["dtype"];
            Tango::CmdArgType item_dtype = bopy::extract<Tango::CmdArgType>(py_item_dtype);
            __append(obj, item_name, py_item_value, item_dtype);
        }
    }
}
*/

template <typename T>
void __set_value(T &obj, bopy::object &py_value)
{
    // need to fill item names first because in case it is a sub-blob,
    // the Tango C++ API doesnt't provide a way to do it
    Py_ssize_t size = bopy::len(py_value);
    std::vector<std::string> elem_names;
    for(Py_ssize_t i = 0; i < size; ++i)
    {
        elem_names.push_back(bopy::extract<std::string>(py_value[i]["name"]));
    }
    obj.set_data_elt_names(elem_names);

    for(Py_ssize_t i = 0; i < size; ++i)
    {
        bopy::object item = py_value[i];
        std::string item_name = bopy::extract<std::string>(item["name"]);
        bopy::object py_item_data = item["value"];
        Tango::CmdArgType item_dtype = bopy::extract<Tango::CmdArgType>(item["dtype"]);
        if(item_dtype == Tango::DEV_PIPE_BLOB) // a sub-blob
        {
            std::string blob_name = bopy::extract<std::string>(py_item_data[0]);
            bopy::object py_blob_data = py_item_data[1];
            Tango::DevicePipeBlob blob(blob_name);
            __set_value(blob, py_blob_data);
            obj << blob;
        }
        else
        {
            __append(obj, item_name, py_item_data, item_dtype);
        }
    }
}

void set_value(Tango::Pipe &pipe, bopy::object &py_value)
{
    __set_value<Tango::Pipe>(pipe, py_value);
}

bopy::object get_value(Tango::WPipe &pipe)
{
    bopy::object py_value;

    Tango::DevicePipeBlob blob = pipe.get_blob();
    py_value = PyTango::DevicePipe::extract(blob);
    return py_value;
}

} // namespace Pipe
} // namespace PyTango

void export_pipe()
{
    bopy::class_<Tango::Pipe, boost::noncopyable>(
        "Pipe", bopy::init<const std::string &, const Tango::DispLevel, bopy::optional<Tango::PipeWriteType>>())

        .def("get_name", &Tango::Pipe::get_name, bopy::return_value_policy<bopy::copy_non_const_reference>())
        .def("set_name", &Tango::Pipe::set_name)
        .def("set_default_properties", &Tango::Pipe::set_default_properties)
        .def("get_root_blob_name",
             &Tango::Pipe::get_root_blob_name,
             bopy::return_value_policy<bopy::copy_const_reference>())
        .def("set_root_blob_name", &Tango::Pipe::set_root_blob_name)
        .def("get_desc", &Tango::Pipe::get_desc, bopy::return_value_policy<bopy::copy_non_const_reference>())
        .def("get_label", &Tango::Pipe::get_label, bopy::return_value_policy<bopy::copy_non_const_reference>())
        .def("get_disp_level", &Tango::Pipe::get_disp_level)
        .def("get_writable", &Tango::Pipe::get_writable)
        .def("get_pipe_serial_model", &Tango::Pipe::get_pipe_serial_model)
        .def("set_pipe_serial_model", &Tango::Pipe::set_pipe_serial_model)
        .def("has_failed", &Tango::Pipe::has_failed)

        .def("_set_value", (void (*)(Tango::Pipe &, bopy::object &)) & PyTango::Pipe::set_value)

        .def("get_value", (bopy::object(*)(Tango::WPipe &)) & PyTango::Pipe::get_value);

    bopy::class_<Tango::WPipe, bopy::bases<Tango::Pipe>, boost::noncopyable>(
        "WPipe", bopy::init<const std::string &, const Tango::DispLevel>());
}

namespace PyDevicePipe
{
static void throw_wrong_python_data_type(const std::string &name, const char *method)
{
    TangoSys_OMemStream o;
    o << "Wrong Python type for pipe " << name << std::ends;
    Tango::Except::throw_exception("PyDs_WrongPythonDataTypeForPipe", o.str(), method);
}

template <typename T, long tangoTypeConst>
void __append_scalar(T &obj, const std::string &name, bopy::object &py_value)
{
    typedef typename TANGO_const2type(tangoTypeConst) TangoScalarType;
    TangoScalarType tg_value;
    from_py<tangoTypeConst>::convert(py_value, tg_value);
    Tango::DataElement<TangoScalarType> data_elt(name, tg_value);
    obj << data_elt;
}

template <typename T, long tangoArrayTypeConst>
void __append_array(T &obj, const std::string &name, bopy::object &py_value)
{
    typedef typename TANGO_const2type(tangoArrayTypeConst) TangoArrayType;

    TangoArrayType *value = fast_convert2array<tangoArrayTypeConst>(py_value);
    obj << value;
}

template <typename T>
bool __check_type(const bopy::object &value)
{
    bopy::extract<T> item(value);
    return item.check();
}

template <typename T>
bool __convert(const bopy::object &value, T &py_item_data)
{
    bopy::extract<T> item(value);
    if(item.check())
    {
        py_item_data = item();
        return true;
    }
    return false;
}

void __append(Tango::DevicePipeBlob &dpb, const std::string &name, bopy::object &value)
{
    if(__check_type<std::string>(value))
    {
        __append_scalar<Tango::DevicePipeBlob, Tango::DEV_STRING>(dpb, name, value);
    }
    else if(__check_type<int>(value))
    {
        __append_scalar<Tango::DevicePipeBlob, Tango::DEV_LONG64>(dpb, name, value);
    }
    else if(__check_type<double>(value))
    {
        __append_scalar<Tango::DevicePipeBlob, Tango::DEV_DOUBLE>(dpb, name, value);
    }
    else if(__check_type<bool>(value))
    {
        __append_scalar<Tango::DevicePipeBlob, Tango::DEV_BOOLEAN>(dpb, name, value);
    }
    else if(__check_type<bopy::list>(value))
    {
        if(__check_type<std::string>(value[0]))
        {
            __append_array<Tango::DevicePipeBlob, Tango::DEVVAR_STRINGARRAY>(dpb, name, value);
        }
        else if(__check_type<int>(value[0]))
        {
            __append_array<Tango::DevicePipeBlob, Tango::DEVVAR_LONG64ARRAY>(dpb, name, value);
        }
        else if(__check_type<double>(value[0]))
        {
            __append_array<Tango::DevicePipeBlob, Tango::DEVVAR_DOUBLEARRAY>(dpb, name, value);
        }
        else
        {
            throw_wrong_python_data_type(name, "__append");
        }
    }
    else
    {
        throw_wrong_python_data_type(name, "__append");
    }
}

void __set_value(Tango::DevicePipeBlob &dpb, bopy::dict &dict)
{
    int nitems = len(dict);
    std::vector<std::string> elem_names;
    for(int i = 0; i < nitems; i++)
    {
        elem_names.push_back(bopy::extract<std::string>(dict.keys()[i]));
    }
    dpb.set_data_elt_names(elem_names);

    bopy::list values = dict.values();
    for(int i = 0; i < nitems; ++i)
    {
        bopy::object item = values[i];
        // Check if the value is an inner blob
        bopy::tuple ptuple;
        std::string blob_name;
        bopy::dict pdict;
        if(__convert(item, ptuple) && __convert(ptuple[0], blob_name) && __convert(ptuple[1], pdict))
        {
            Tango::DevicePipeBlob inner_blob(blob_name);
            __set_value(inner_blob, pdict);
            dpb << inner_blob;
        }
        else
        {
            __append(dpb, elem_names[i], item);
        }
    }
}

void set_value(Tango::DevicePipeBlob &dpb, bopy::object &py_data)
{
    std::string name = bopy::extract<std::string>(py_data[0]);
    dpb.set_name(name);

    bopy::dict data = bopy::extract<bopy::dict>(py_data[1]);
    __set_value(dpb, data);
}
} // namespace PyDevicePipe
