#include "filedb/database_device.h"

#include <tango/tango.h>

#include <functional>

namespace FileDb
{

DatabaseDeviceBackend::~DatabaseDeviceBackend() { }

namespace detail
{

template <typename F>
struct member_fn_traits;

template <>
struct member_fn_traits<std::nullptr_t>
{
    using class_type = void;
};

template <typename C, typename R, typename... Args>
struct member_fn_traits<R (C::*)(Args... args)>
{
    using class_type = C;
    using return_type = R;
    constexpr static int arity = std::tuple_size<std::tuple<Args...>>();
    template <int N>
    using argument_type = std::tuple_element_t<N, std::tuple<Args...>>;
};

template <typename C, typename R, typename... Args>
struct member_fn_traits<R (C::*)(Args... args) const>
{
    using class_type = C;
    using return_type = R;
    constexpr static int arity = std::tuple_size<std::tuple<Args...>>();
    template <int N>
    using argument_type = std::tuple_element_t<N, std::tuple<Args...>>;
};
} // namespace detail

template <auto cmd_fn>
class DatabaseDeviceCommand : public Tango::Command
{
  public:
    using traits = detail::member_fn_traits<decltype(cmd_fn)>;
    using Device = DatabaseDevice;
    using Tango::Command::Command;

    DatabaseDeviceCommand(const std::string &name, const std::string &in_desc, const std::string &out_desc) :
        Tango::Command(name, type_in(), type_out(), in_desc, out_desc, Tango::OPERATOR)
    {
    }

    ~DatabaseDeviceCommand() override { }

    CORBA::Any *execute(Tango::DeviceImpl *dev, const CORBA::Any &in_any) override
    {
        if constexpr(traits::arity == 0 && std::is_same_v<void, typename traits::return_type>)
        {
            std::invoke(cmd_fn, static_cast<Device *>(dev)->m_backend);
            return insert();
        }
        else if constexpr(traits::arity == 0)
        {
            auto ret = std::invoke(cmd_fn, static_cast<Device *>(dev)->m_backend);
            return insert(ret);
        }
        else if constexpr(std::is_same_v<void, typename traits::return_type>)
        {
            using arg_type = std::remove_cv_t<std::remove_reference_t<typename traits::template argument_type<0>>>;
            if constexpr(std::is_same_v<arg_type, typename Tango::tango_type_traits<arg_type>::ArrayType>)
            {
                const arg_type *arg;
                extract(in_any, arg);
                std::invoke(cmd_fn, static_cast<Device *>(dev)->m_backend, *arg);
            }
            else
            {
                arg_type arg;
                extract(in_any, arg);
                std::invoke(cmd_fn, static_cast<Device *>(dev)->m_backend, arg);
            }
            return insert();
        }
        else
        {
            using arg_type = std::remove_cv_t<std::remove_reference_t<typename traits::template argument_type<0>>>;
            auto ret = [this, &dev, &in_any]()
            {
                if constexpr(std::is_same_v<arg_type, typename Tango::tango_type_traits<arg_type>::ArrayType>)
                {
                    const arg_type *arg;
                    extract(in_any, arg);
                    return std::invoke(cmd_fn, static_cast<Device *>(dev)->m_backend, *arg);
                }
                else
                {
                    arg_type arg;
                    extract(in_any, arg);
                    return std::invoke(cmd_fn, static_cast<Device *>(dev)->m_backend, arg);
                }
            }();

            return insert(ret);
        }
    }

  private:
    constexpr static Tango::CmdArgType type_out()
    {
        if constexpr(std::is_same_v<void, typename traits::return_type>)
        {
            return Tango::DEV_VOID;
        }
        else if constexpr(std::is_same_v<Tango::DevVarStringArray *, typename traits::return_type>)
        {
            return Tango::DEVVAR_STRINGARRAY;
        }
        else if constexpr(std::is_same_v<Tango::DevVarLongStringArray *, typename traits::return_type>)
        {
            return Tango::DEVVAR_LONGSTRINGARRAY;
        }
        else
        {
            static_assert(!std::is_same_v<typename traits::return_type, typename traits::return_type>);
        }
    }

    constexpr static Tango::CmdArgType type_in()
    {
        if constexpr(traits::arity == 0)
        {
            return Tango::DEV_VOID;
        }
        else
        {
            using arg_type = std::remove_cv_t<std::remove_reference_t<typename traits::template argument_type<0>>>;
            return Tango::tango_type_traits<arg_type>::type_value();
        }
    }
};

void DatabaseDeviceClass::command_factory()
{
    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::info>{
        "DbInfo",
        "",
        "Miscellaneous info like:\n- Device defined in database\n- Device marked as exported in "
        "database\n- Device server process defined in database\n- Device server process marked "
        "as exported in database\n- Device properties defined in database\n- Class properties "
        "defined in database\n- Device attribute properties defined in database\n- Class "
        "attribute properties defined in database\n- Object properties defined in database"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_property>{
        "DbGetProperty",
        "Str[0] = Object name\nStr[1] = Property name\nStr[n] = Property name",
        "Str[0] = Object name\nStr[1] = Property number\nStr[2] = Property name\nStr[3] = "
        "Property value number (array case)\nStr[4] = Property value 1\nStr[n] = Property value "
        "n (array case)\nStr[n + 1] = Property name\nStr[n + 2] = Property value number (array "
        "case)\nStr[n + 3] = Property value 1\nStr[n + m] = Property value m"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::put_property>{
        "DbPutProperty",
        "Str[0] = Object name\nStr[1] = Property number\nStr[2] = Property name\nStr[3] = Property value "
        "number\nStr[4] = Property value 1\nStr[n] = Property value n\n....",
        ""});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_device_property>{
        "DbGetDeviceProperty",
        "Str[0] = Device name\nStr[1] = Property name\nStr[n] = Property name",
        "Str[0] = Device name\nStr[1] = Property number\nStr[2] = Property name\nStr[3] = "
        "Property value number (array case)\nStr[4] = Property value 1\nStr[n] = Property value "
        "n (array case)\nStr[n + 1] = Property name\nStr[n + 2] = Property value number (array "
        "case)\nStr[n + 3] = Property value 1\nStr[n + m] = Property value m"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_class_property>{
        "DbGetClassProperty",
        "Str[0] = Tango class\nStr[1] = Property name\nStr[2] = Property name",
        "Str[0] = Tango class\nStr[1] = Property number\nStr[2] = Property name\nStr[3] = Property value number "
        "(array case)\nStr[4] = Property value\nStr[n] = Property value (array case)\n...."});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_device_attribute_property2>{
        "DbGetDeviceAttributeProperty2",
        "Str[0] = Device name\nStr[1] = Attribute name\nStr[n] = Attribute name",
        "Str[0] = Device name\nStr[1] = Attribute property  number\nStr[2] = Attribute property 1 name\nStr[3] = "
        "Attribute property 1 value number (array case)\nStr[4] = Attribute property 1 value\nStr[n] = Attribute "
        "property 1 value (array case)\nStr[n + 1] = Attribute property 2 name\nStr[n + 2] = Attribute property 2 "
        "value number (array case)\nStr[n + 3] = Attribute property 2 value\nStr[n + m] = Attribute property 2 value "
        "(array case)"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_class_attribute_property2>{
        "DbGetClassAttributeProperty2",
        "Str[0] = Device name\nStr[1] = Attribute name\nStr[n] = Attribute name",
        "Str[0] = Device name\nStr[1] = Attribute property  number\nStr[2] = Attribute property 1 name\nStr[3] = "
        "Attribute property 1 value number (array case)\nStr[4] = Attribute property 1 value\nStr[n] = Attribute "
        "property 1 value (array case)\nStr[n + 1] = Attribute property 2 name\nStr[n + 2] = Attribute property 2 "
        "value number (array case)\nStr[n + 3] = Attribute property 2 value\nStr[n + m] = Attribute property 2 value "
        "(array case)"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::put_device_property>{
        "DbPutDeviceProperty",
        "Str[0] = Tango device name\nStr[1] = Property number\nStr[2] = Property name\nStr[3] = Property value "
        "number\nStr[4] = Property value 1\nStr[n] = Property value n\n....",
        ""});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::delete_device_property>{
        "DbDeleteDeviceProperty", "Str[0] = Device name\nStr[1] = Property name\nStr[n] = Property name", ""});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_class_pipe_property>{
        "DbGetClassPipeProperty",
        "Str[0] = Tango class name\nStr[1] = Pipe name\nStr[n] = Pipe name",
        "Str[0] = Tango class name\nStr[1] = Pipe property  number\nStr[2] = Pipe property 1 name\nStr[3] = Pipe "
        "property 1 value number (array case)\nStr[4] = Pipe property 1 value\nStr[n] = Pipe property 1 value "
        "(array case)\nStr[n + 1] = Pipe property 2 name\nStr[n + 2] = Pipe property 2 value number (array "
        "case)\nStr[n + 3] = Pipe property 2 value\nStr[n + m] = Pipe property 2 value (array case)"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_device_pipe_property>{
        "DbGetDevicePipeProperty",
        "Str[0] = Device name\nStr[1] = Pipe name\nStr[n] = Pipe name",
        "Str[0] = Device name\nStr[1] = Pipe property  number\nStr[2] = Pipe property 1 name\nStr[3] = Pipe "
        "property 1 value number (array case)\nStr[4] = Pipe property 1 value\nStr[n] = Pipe property 1 value "
        "(array case)\nStr[n + 1] = Pipe property 2 name\nStr[n + 2] = Pipe property 2 value number (array "
        "case)\nStr[n + 3] = Pipe property 2 value\nStr[n + m] = Pipe property 2 value (array case)"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_device_list>{
        "DbGetDeviceList",
        "argin[0] : server name\nargin[1] : class name",
        "The list of devices for specified server and class."});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_device_domain_list>{
        "DbGetDeviceDomainList", "The wildcard", "Device name domain list"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_device_family_list>{
        "DbGetDeviceFamilyList", "The wildcard", "Device name family list"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_device_member_list>{
        "DbGetDeviceMemberList", "The wildcard", "Device name member list"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_device_wide_list>{
        "DbGetDeviceWideList", "The wildcard", "Device name list"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_device_exported_list>{
        "DbGetDeviceExportedList", "The wildcard", "Device name list"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_device_property_list>{
        "DbGetDevicePropertyList", "Str[0] = device name\nStr[1] = Filter", "Property name list"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::import_device>{
        "DbImportDevice",
        "Device name (or alias)",
        "Str[0] = device name\nStr[1] = CORBA IOR\nStr[2] = device version\nStr[3] = "
        "device server process name\nStr[4] = host name\nStr[5] = Tango class "
        "name\n\nLg[0] = Exported flag\nLg[1] = Device server process PID"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::export_device>{
        "DbExportDevice",
        "Str[0] = Device name\nStr[1] = CORBA IOR\nStr[2] = Device server process host name\nStr[3] = Device "
        "server process PID or string ``null``\nStr[4] = Device server process version",
        ""});

    command_list.push_back(
        new DatabaseDeviceCommand<&DatabaseDeviceBackend::unexport_device>{"DbUnExportDevice", "Device name", ""});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::unexport_server>{
        "DbUnExportServer", "Device server name (executable/instance)", ""});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_server_list>{
        "DbGetServerList", "The filter", "Device server process name list"});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_server_name_list>{
        "DbGetServerNameList", "wildcard for server names.", "server names found."});

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::get_device_class_list>{
        "DbGetDeviceClassList",
        "Device server process name",
        "Str[0] = Device name\nStr[1] = Tango class\nStr[n] = Device name\nStr[n + 1] = Tango class",
    });

    command_list.push_back(new DatabaseDeviceCommand<&DatabaseDeviceBackend::add_server>{
        "DbAddServer",
        "Str[0] = Full device server name\nStr[1] = Device(s) name\nStr[2] = Tango class name\nStr[n] = Device "
        "name\nStr[n + 1] = Tango class name",
        ""});

    command_list.push_back(
        new DatabaseDeviceCommand<&DatabaseDeviceBackend::delete_server>{"DbDeleteServer", "Device server name", ""});
}

void DatabaseDeviceClass::device_factory([[maybe_unused]] const Tango::DevVarStringArray *devlist_ptr)
{
    auto *device = new DatabaseDevice{this, db_name.c_str()};
    device->m_backend.reset(backend_factory());

    device_list.push_back(device);
    export_device(device_list.back(), "database");
}

void DatabaseDeviceClass::initialise_database()
{
    auto *tg = Tango::Util::instance();

    Tango::DeviceImpl *dbase = tg->get_device_by_name(db_name);
    auto ior = std::unique_ptr<char[]>{tg->get_orb()->object_to_string(dbase->get_d_var())};
    Tango::DeviceImpl *dserver = tg->get_dserver_device();
    auto dserver_ior = std::unique_ptr<char[]>{tg->get_orb()->object_to_string(dserver->_this())};

    static_cast<DatabaseDevice *>(dbase)->m_backend->export_self(this->name,
                                                                 db_name,
                                                                 dserver->get_name(),
                                                                 ior.get(),
                                                                 dserver_ior.get(),
                                                                 tg->get_host_name(),
                                                                 tg->get_pid(),
                                                                 tg->get_version_str());
}

DatabaseDeviceClass *DatabaseDeviceClass::_instance = nullptr;
std::string DatabaseDeviceClass::db_name{"sys/database/"};
} // namespace FileDb
