#include "filedb/filedb_device.h"
#include "filedb/database_tables.h"
#include "filedb/internal/utils.h"

#include <tango/tango.h>

namespace FileDb
{

namespace
{

char *time_to_corba_string(const std::chrono::system_clock::time_point &tp)
{
    std::stringstream ss;
    auto time = std::chrono::system_clock::to_time_t(tp);
    ss << std::put_time(std::gmtime(&time), "%Y-%m-%dT%H:%M:%S");
    std::string str = ss.str();
    auto t = std::chrono::system_clock::to_time_t(tp);
    char *result = CORBA::string_alloc(str.size());
    strncpy(result, str.c_str(), str.size());
    return result;
}

template <typename Record>
Tango::DevVarStringArray *pull_properties_from_table(cistring_view ident_prefix,
                                                     const std::vector<Record> &table,
                                                     const Tango::DevVarStringArray &in,
                                                     size_t header_size = 1)
{
    Tango::DevVarStringArray_var result = new Tango::DevVarStringArray;
    auto &out = result.inout();

    unsigned long num_prop = in.length() - header_size;

    // Allocate space for the properties up-front to avoid too may allocations.
    //
    // Here we are allocating the minimum amount of space required for each
    // property (3) plus object and property number (2). That is, slots for:
    //
    //  - The name
    //  - The number of elements of the value
    //  - At least one element (this is expected even if the number of elements is
    //  zero)
    //
    // Later we allocate more space if we find that there are multiple elements
    // for a value.
    out.length(2 + (3 * num_prop));

    size_t in_index = 0;
    size_t out_index = 0;
    for(; in_index < header_size; ++in_index)
    {
        out[out_index] = string_dup(in[in_index]);
        ++out_index;
    }
    out[out_index] = string_dup(std::to_string(num_prop).c_str());
    ++out_index;

    cistringstream ss;
    for(; in_index < in.length(); ++in_index)
    {
        out[out_index] = string_dup(in[in_index].in());
        ++out_index;

        ss << ident_prefix << "/" << in[in_index].in();
        cistring needle = ss.str();
        ss = {};
        auto found = std::lower_bound(table.begin(), table.end(), needle, IdentCompare{});

        if(found == table.end() || !ident_equiv(*found, needle))
        {
            out[out_index] = string_dup("0");
            ++out_index;
            out[out_index] = string_dup(" ");
            ++out_index;
        }
        else
        {
            size_t value_len = found->values.size();

            if(value_len > 1)
            {
                // Correct our assumption above
                out.length(out.length() + value_len - 1);
            }

            out[out_index] = string_dup(std::to_string(value_len).c_str());
            ++out_index;

            for(size_t i = 0; i < value_len; ++i)
            {
                out[out_index] = string_dup(found->values[i].c_str());
                ++out_index;
            }
        }
    }

    return result._retn();
}

template <typename Record>
Tango::DevVarStringArray *pull_attribute_properties_from_table(cistring_view ident_prefix,
                                                               const std::vector<Record> &table,
                                                               const Tango::DevVarStringArray &in,
                                                               size_t header_size = 1)
{
    constexpr const size_t ident_count = Record::ident_count - 1;

    Tango::DevVarStringArray_var result = new Tango::DevVarStringArray;
    auto &out = result.inout();

    unsigned long num_attr = in.length() - header_size;

    // Allocate space for all the attributes, assuming they all have to
    // properties.
    //
    // Here we are allocating the minimum amount of space required for each
    // attribute (2) plus object and attribute number (2). That is, slots for:
    //
    //  - The attribute name
    //  - The number of properties
    //
    // Later we allocate more space if we find that there are multiple elements
    // for a value.
    out.length(2 + (2 * num_attr));

    size_t in_index = 0;
    size_t out_index = 0;
    for(; in_index < header_size; ++in_index)
    {
        out[out_index] = string_dup(in[in_index]);
        ++out_index;
    }
    out[out_index] = string_dup(std::to_string(num_attr).c_str());
    ++out_index;

    cistringstream ss;
    for(; in_index < in.length(); ++in_index)
    {
        out[out_index] = string_dup(in[in_index].in());
        ++out_index;

        ss << ident_prefix << "/" << in[in_index].in();
        cistring needle = ss.str();
        ss = {};
        auto found = std::lower_bound(table.begin(), table.end(), needle, IdentCompare<ident_count>{});

        if(found == table.end() || !ident_equiv(*found, needle, ident_count))
        {
            out[out_index] = string_dup("0");
            ++out_index;
        }
        else
        {
            size_t total_value_count = 0;
            auto attr_end = std::find_if(found,
                                         table.end(),
                                         [=, &total_value_count, &needle](const Record &record)
                                         {
                                             total_value_count += record.values.size();
                                             return !ident_equiv(needle, record, ident_count);
                                         });

            size_t prop_count = attr_end - found;
            out[out_index] = string_dup(std::to_string(prop_count).c_str());
            ++out_index;

            // Allocate space for the attribute property values
            out.length(out.length() + 2 * prop_count + total_value_count);

            for(auto prop = found; prop != attr_end; ++prop)
            {
                out[out_index] = string_dup(prop->property());
                ++out_index;

                size_t value_count = prop->values.size();

                out[out_index] = string_dup(std::to_string(value_count).c_str());
                ++out_index;

                for(size_t i = 0; i < value_count; ++i)
                {
                    out[out_index] = string_dup(prop->values[i].c_str());
                    ++out_index;
                }
            }
        }
    }

    return result._retn();
}

} // namespace

FileDbDeviceBackend::FileDbDeviceBackend(const char *filename, bool save) :
    m_filename{filename},
    m_save{save}
{
    if(m_filename[0] == '\0')
    {
        m_save = false;
    }
    else
    {
        m_config_tables.load(m_filename.c_str());
    }

    const auto &device_definitions = m_config_tables.get_table<ServerRecord>();

    for(const auto &dd : device_definitions)
    {
        add_devices_from_server(dd);
    }
}

FileDbDeviceBackend::FileDbDeviceBackend(DbConfigTables &&initial_tables) :
    m_filename{""},
    m_save{false},
    m_config_tables{std::move(initial_tables)}
{
}

FileDbDeviceBackend::FileDbDeviceBackend(DbRuntimeTables &&initial_tables) :
    m_filename{""},
    m_save{false},
    m_runtime_tables{std::move(initial_tables)}
{
}

FileDbDeviceBackend::FileDbDeviceBackend(DbConfigTables &&initial_config_tables,
                                         DbRuntimeTables &&initial_runtime_tables) :
    m_filename{""},
    m_save{false},
    m_config_tables{std::move(initial_config_tables)},
    m_runtime_tables{std::move(initial_runtime_tables)}
{
}

void FileDbDeviceBackend::add_devices_from_server(const ServerRecord &dd)
{
    for(const auto &device : dd.devices)
    {
        DeviceRecord record;
        record.ident = cistring{device.data(), device.size()};
        record.exported = false;
        record.server = dd.server();
        record.device_class = dd.device_class();

        m_runtime_tables.put_record(record);
    }
}

/**
 *	Command DbInfo related method
 *	Description: Get miscellaneous numbers on information
 *               stored in database
 *
 *	@returns Miscellaneous info like:
 *           - Device defined in database
 *           - Device marked as exported in database
 *           - Device server process defined in database
 *           - Device server process marked as exported in database
 *           - Device properties defined in database
 *           - Class properties defined in database
 *           - Device attribute properties defined in database
 *           - Class attribute properties defined in database
 *           - Object properties defined in database
 */
Tango::DevVarStringArray *FileDbDeviceBackend::info() const
{
    Tango::DevVarStringArray_var result = new Tango::DevVarStringArray;
    auto &out = result.inout();

    out.length(13);

    auto to_corba_string = [](auto &&...args)
    {
        std::stringstream ss;
        (ss << ... << args);
        return string_dup(ss.str().c_str());
    };

    if(m_filename.empty())
    {
        out[0] = to_corba_string("Experimental FileDbDs (memory mode)");
    }
    else if(m_save)
    {
        out[0] = to_corba_string("Experimental FileDbDs \"", m_filename, "\"");
    }
    else
    {
        out[0] = to_corba_string("Experimental FileDbDs \"", m_filename, "\" (init only mode)");
    }
    out[1] = string_dup("");

    {
        DeviceRecord record;
        record.ident = cistring{DatabaseDeviceClass::db_name.data(), DatabaseDeviceClass::db_name.size()};
        const auto &table = m_runtime_tables.get_table<DeviceRecord>();
        auto found = std::lower_bound(table.begin(), table.end(), record, IdentCompare<1>{});

        if(found != table.end() && ident_equiv(*found, record) && found->started)
        {
            auto time = std::unique_ptr<char[]>(time_to_corba_string(*found->started));
            out[2] = to_corba_string("Running since ", time.get());
        }
        else
        {
            out[2] = to_corba_string("Running since ----");
        }
    }

    out[3] = string_dup("");

    {
        const auto &table = m_runtime_tables.get_table<DeviceRecord>();
        out[4] = to_corba_string("Devices defined = ", table.size());
    }

    {
        const auto &table = m_runtime_tables.get_table<DeviceRecord>();
        out[5] = to_corba_string(
            "Devices exported = ",
            std::count_if(table.begin(), table.end(), [](const auto &record) { return record.exported; }));
    }

    {
        const auto &table = m_runtime_tables.get_table<DeviceRecord>();

        out[6] = to_corba_string(
            "Device servers defined = ",
            std::count_if(table.begin(), table.end(), [](const auto &record) { return record.server == "DServer"; }));
    }

    {
        const auto &table = m_runtime_tables.get_table<DeviceRecord>();

        out[6] = to_corba_string("Device servers defined = ",
                                 std::count_if(table.begin(),
                                               table.end(),
                                               [](const auto &record)
                                               { return record.server == "DServer" && record.exported; }));
    }

    out[8] = string_dup("");

    {
        using Record = ClassPropertyRecord;
        const auto &table = m_config_tables.get_table<Record>();

        out[9] = to_corba_string("Class properties defined = ", table.size());
    }

    {
        using Record = DevicePropertyRecord;
        const auto &table = m_config_tables.get_table<Record>();

        out[10] = to_corba_string("Device properties defined = ", table.size());
    }

    {
        using Record = ClassAttributePropertyRecord;
        const auto &table = m_config_tables.get_table<Record>();

        out[9] = to_corba_string("Class attribute properties defined = ", table.size());
    }

    {
        using Record = DeviceAttributePropertyRecord;
        const auto &table = m_config_tables.get_table<Record>();

        out[10] = to_corba_string("Device attribute properties defined = ", table.size());
    }

    return result._retn();
}

/**
 *	Command DbGetProperty related method
 *	Description: Get free object property
 *
 *	@param argin Str[0] = Object name
 *               Str[1] = Property name
 *               Str[n] = Property name
 *	@returns Str[0] = Object name
 *           Str[1] = Property number
 *           Str[2] = Property name
 *           Str[3] = Property value number (array case)
 *           Str[4] = Property value 1
 *           Str[n] = Property value n (array case)
 *           Str[n + 1] = Property name
 *           Str[n + 2] = Property value number (array case)
 *           Str[n + 3] = Property value 1
 *           Str[n + m] = Property value m
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_property(const Tango::DevVarStringArray &in) const
{
    using Record = FreeObjectPropertyRecord;

    if(in.length() < 1)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments passed to FileDbDeviceBackend::get_property. Expecting at least 1, "
              "found "
           << in.length();
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    cistring_view prefix = in[0].in();
    const std::vector<Record> &table = m_config_tables.get_table<Record>();

    return pull_properties_from_table(prefix, table, in);
}

/**
 *	Command DbGetDeviceProperty related method
 *	Description:
 *
 *	@param argin Str[0] = Device name
 *               Str[1] = Property name
 *               Str[n] = Property name
 *	@returns Str[0] = Device name
 *           Str[1] = Property number
 *           Str[2] = Property name
 *           Str[3] = Property value number (array case)
 *           Str[4] = Property value 1
 *           Str[n] = Property value n (array case)
 *           Str[n + 1] = Property name
 *           Str[n + 2] = Property value number (array case)
 *           Str[n + 3] = Property value 1
 *           Str[n + m] = Property value m
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_device_property(const Tango::DevVarStringArray &in) const
{
    using Record = DevicePropertyRecord;

    if(in.length() < 2)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments passed to FileDbDeviceBackend::get_device_property. Expecting at least "
              "2, "
              "found "
           << in.length();
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    cistring_view prefix = in[0].in();
    const std::vector<Record> &table = m_config_tables.get_table<Record>();

    return pull_properties_from_table(prefix, table, in);
}

/**
 *	Command DbGetClassProperty related method
 *	Description:
 *
 *	@param argin Str[0] = Tango class
 *               Str[1] = Property name
 *               Str[2] = Property name
 *	@returns Str[0] = Tango class
 *           Str[1] = Property number
 *           Str[2] = Property name
 *           Str[3] = Property value number (array case)
 *           Str[4] = Property value
 *           Str[n] = Property value (array case)
 *           ....
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_class_property(const Tango::DevVarStringArray &in) const
{
    using Record = ClassPropertyRecord;

    if(in.length() < 1)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments passed to FileDbDeviceBackend::get_class_property. Expecting at least "
              "1, "
              "found "
           << in.length();
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    cistring_view prefix = in[0].in();
    const std::vector<Record> &table = m_config_tables.get_table<Record>();

    return pull_properties_from_table(prefix, table, in);
}

/**
 *	Command DbGetDeviceAttributeProperty2 related method
 *	Description: Retrieve device attribute properties. This command has the possibility to retrieve
 *               device attribute properties which are arrays. It is not possible with the old
 *               DbGetDeviceAttributeProperty command. Nevertheless, the old command has not been
 *               deleted for compatibility reason
 *
 *	@param argin Str[0] = Device name
 *               Str[1] = Attribute name
 *               Str[n] = Attribute name
 *	@returns Str[0] = Device name
 *           Str[1] = Attribute property  number
 *           Str[2] = Attribute property 1 name
 *           Str[3] = Attribute property 1 value number (array case)
 *           Str[4] = Attribute property 1 value
 *           Str[n] = Attribute property 1 value (array case)
 *           Str[n + 1] = Attribute property 2 name
 *           Str[n + 2] = Attribute property 2 value number (array case)
 *           Str[n + 3] = Attribute property 2 value
 *           Str[n + m] = Attribute property 2 value (array case)
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_device_attribute_property2(const Tango::DevVarStringArray &in) const
{
    using Record = DeviceAttributePropertyRecord;

    if(in.length() < 1)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments passed to FileDbDeviceBackend::get_device_attribute_property2. "
              "Expecting at "
              "least 1, found "
           << in.length();
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    cistring_view prefix = in[0].in();
    const std::vector<Record> &table = m_config_tables.get_table<Record>();

    return pull_attribute_properties_from_table(prefix, table, in);
}

/**
 *	Command DbGetClassAttributeProperty2 related method
 *	Description: This command supports array property compared to the old command called
 *               DbGetClassAttributeProperty. The old command has not been deleted from the
 *               server for compatibility reasons.
 *
 *	@param argin Str[0] = Tango class name
 *               Str[1] = Attribute name
 *               Str[n] = Attribute name
 *	@returns Str[0] = Tango class name
 *           Str[1] = Attribute property  number
 *           Str[2] = Attribute property 1 name
 *           Str[3] = Attribute property 1 value number (array case)
 *           Str[4] = Attribute property 1 value
 *           Str[n] = Attribute property 1 value (array case)
 *           Str[n + 1] = Attribute property 2 name
 *           Str[n + 2] = Attribute property 2 value number (array case)
 *           Str[n + 3] = Attribute property 2 value
 *           Str[n + m] = Attribute property 2 value (array case)
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_class_attribute_property2(const Tango::DevVarStringArray &in) const
{
    using Record = ClassAttributePropertyRecord;

    if(in.length() < 1)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments passed to FileDbDeviceBackend::get_class_attribute_property2. Expecting "
              "at "
              "least 1, found "
           << in.length();
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    cistring_view prefix = in[0].in();
    const std::vector<Record> &table = m_config_tables.get_table<Record>();

    return pull_attribute_properties_from_table(prefix, table, in);
}

/**
 *	Command DbGetDevicePipeProperty related method
 *	Description: Retrieve device pipe properties
 *
 *	@param argin Str[0] = Device name
 *               Str[1] = Pipe name
 *               Str[n] = Pipe name
 *	@returns Str[0] = Device name
 *           Str[1] = Pipe property  number
 *           Str[2] = Pipe property 1 name
 *           Str[3] = Pipe property 1 value number (array case)
 *           Str[4] = Pipe property 1 value
 *           Str[n] = Pipe property 1 value (array case)
 *           Str[n + 1] = Pipe property 2 name
 *           Str[n + 2] = Pipe property 2 value number (array case)
 *           Str[n + 3] = Pipe property 2 value
 *           Str[n + m] = Pipe property 2 value (array case)
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_device_pipe_property(const Tango::DevVarStringArray &in) const
{
    Tango::DevVarStringArray_var result = new Tango::DevVarStringArray;
    auto &out = result.inout();

    if(in.length() < 1)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments passed to FileDbDeviceBackend::get_device_pipe_property. Expecting at "
              "least 1, found "
           << in.length();
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    out.length(2);
    out[0] = string_dup(in[0].in());
    out[1] = string_dup("0");

    return result._retn();
}

/**
 *	Command DbGetClassPipeProperty related method
 *	Description: Retrieve class pipe properties
 *
 *	@param argin Str[0] = Tango class name
 *               Str[1] = Pipe name
 *               Str[n] = Pipe name
 *	@returns Str[0] = Tango class name
 *           Str[1] = Pipe property  number
 *           Str[2] = Pipe property 1 name
 *           Str[3] = Pipe property 1 value number (array case)
 *           Str[4] = Pipe property 1 value
 *           Str[n] = Pipe property 1 value (array case)
 *           Str[n + 1] = Pipe property 2 name
 *           Str[n + 2] = Pipe property 2 value number (array case)
 *           Str[n + 3] = Pipe property 2 value
 *           Str[n + m] = Pipe property 2 value (array case)
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_class_pipe_property(const Tango::DevVarStringArray &in) const
{
    Tango::DevVarStringArray_var result = new Tango::DevVarStringArray;
    auto &out = result.inout();

    if(in.length() < 1)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments passed to FileDbDeviceBackend::get_class_pipe_property. Expecting at "
              "least 1, found "
           << in.length();
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    out.length(2);
    out[0] = string_dup(in[0].in());
    out[1] = string_dup("0");

    return result._retn();
}

/**
 *	Command DbGetDeviceList related method
 *	Description: Get a list of devices for specified server and class.
 *
 *	@param argin argin[0] : server name
 *               argin[1] : class name
 *	@returns The list of devices for specified server and class.
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_device_list(const Tango::DevVarStringArray &in) const
{
    using Record = ServerRecord;

    if(in.length() != 2)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments. Expecting 2, found " << in.length() << ".";
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    std::stringstream ss;
    ss << in[0].in() << "/" << in[1].in();
    Wildcard wildcard{ss.str()};
    const std::vector<Record> &table = m_config_tables.get_table<Record>();

    Tango::DevVarStringArray_var result = new Tango::DevVarStringArray;
    auto &out = result.inout();
    size_t out_index = 0;

    for(auto it = table.begin(); it != table.end(); ++it, it = std::find_if(it, table.end(), wildcard))
    {
        out.length(out.length() + it->devices.size());

        for(const auto &device : it->devices)
        {
            std::stringstream ss;
            ss << device;
            out[out_index] = string_dup(ss.str());
            ++out_index;
        }
    }

    return result._retn();
}

namespace
{
template <typename Pred, size_t N = std::numeric_limits<size_t>::max()>
Tango::DevVarStringArray *pull_device_names(Pred pred, const std::vector<DeviceRecord> &table)
{
    auto get_name = [](const DeviceRecord &record)
    {
        if constexpr(N == std::numeric_limits<size_t>::max())
        {
            return cistring_view{record.device()};
        }
        else
        {
            return record.view_ident_parts(N);
        }
    };

    std::vector<cistring_view> names;
    for(const auto &record : table)
    {
        if constexpr(!std::is_same_v<Pred, std::nullptr_t>)
        {
            if(pred(record))
            {
                names.push_back(get_name(record));
            }
        }
        else
        {
            names.push_back(get_name(record));
        }
    }

    // The device names are already sorted and unique.
    if constexpr(N != std::numeric_limits<size_t>::max())
    {
        // First ident_part is guaranteed to be sorted, because they were sorted in
        // the table.
        if constexpr(N != 0)
        {
            std::sort(names.begin(), names.end());
        }
        names.erase(std::unique(names.begin(), names.end()), names.end());
    }

    Tango::DevVarStringArray_var result = new Tango::DevVarStringArray;
    auto &out = result.inout();
    size_t out_index = 0;

    out.length(names.size());
    for(auto name : names)
    {
        out[out_index] = string_dup(name);
        ++out_index;
    }

    return result._retn();
}
} // namespace

/**
 *	Command DbGetDeviceDomainList related method
 *	Description: Get list of device domain name matching the specified
 *
 *	@param argin The wildcard
 *	@returns Device name domain list
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_device_domain_list(const Tango::DevString &in) const
{
    const auto &table = m_runtime_tables.get_table<DeviceRecord>();
    return pull_device_names<Wildcard, 0>(Wildcard{in}, table);
}

/**
 *	Command DbGetDeviceFamilyList related method
 *	Description: Get a list of device name families for device name matching the
 *               specified wildcard
 *
 *	@param argin The wildcard
 *	@returns Family list
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_device_family_list(const Tango::DevString &in) const
{
    const auto &table = m_runtime_tables.get_table<DeviceRecord>();
    return pull_device_names<Wildcard, 1>(Wildcard{in}, table);
}

/**
 *	Command DbGetDeviceMemberList related method
 *	Description: Get a list of device name members for device name matching the
 *               specified filter
 *
 *	@param argin The filter
 *	@returns Device names member list
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_device_member_list(const Tango::DevString &in) const
{
    const auto &table = m_runtime_tables.get_table<DeviceRecord>();
    return pull_device_names<Wildcard, 2>(Wildcard{in}, table);
}

/**
 *	Command DbGetDeviceWideList related method
 *	Description: Get a list of devices whose names satisfy the filter.
 *
 *	@param argin filter
 *	@returns list of exported devices
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_device_wide_list(const Tango::DevString &in) const
{
    const auto &table = m_runtime_tables.get_table<DeviceRecord>();
    return pull_device_names(Wildcard{in}, table);
}

/**
 *	Command DbGetDeviceExportedList related method
 *	Description: Get a list of exported devices whose names satisfy the filter (wildcard is
 *
 *	@param argin filter
 *	@returns list of exported devices
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_device_exported_list(const Tango::DevString &in) const
{
    const auto &table = m_runtime_tables.get_table<DeviceRecord>();
    Wildcard matcher{in};
    return pull_device_names([&matcher](const auto &record) { return record.exported && matcher(record.ident); },
                             table);
}

/**
 *	Command DbGetDevicePropertyList related method
 *	Description: Get property list belonging to the specified device and with
 *               name matching the specified filter
 *
 *	@param argin Str[0] = device name
 *               Str[1] = Filter
 *	@returns Property name list
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_device_property_list(const Tango::DevVarStringArray &in) const
{
    if(in.length() != 2)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments. Expecting 2, found " << in.length() << ".";
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    Tango::DevVarStringArray_var result = new Tango::DevVarStringArray;
    auto &out = result.inout();

    std::stringstream ss;
    // TODO: Check in[0] does not contain a *
    ss << in[0].in() << "/";
    if(in[1] == nullptr)
    {
        ss << "*";
    }
    else
    {
        ss << in[1].in();
    }
    Wildcard matcher{ss.str()};

    const auto &table = m_config_tables.get_table<DevicePropertyRecord>();
    cistring_view device = in[0].in();

    auto begin = std::lower_bound(table.begin(), table.end(), device, IdentCompare<3>{});
    auto end = begin;
    for(; end != table.end() && ident_equiv(*end, device, 3); ++end)
    {
    }
    out.length(end - begin);

    size_t out_index = 0;
    for(auto it = begin; it != end; ++it, it = std::find_if(it, end, matcher))
    {
        out[out_index] = string_dup(it->property());
        ++out_index;
    }

    out.length(out_index);
    return result._retn();
}

/**
 *	Command DbGetServerList related method
 *	Description: Get list of device server process defined in database
 *               with name matching the specified filter
 *
 *	@param argin The filter
 *	@returns Device server process name list
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_server_list(const Tango::DevString &in) const
{
    using Record = ServerRecord;

    Wildcard matcher{in == nullptr ? "*" : in};
    const std::vector<Record> &table = m_config_tables.get_table<Record>();

    Tango::DevVarStringArray_var result = new Tango::DevVarStringArray;
    auto &out = result.inout();
    size_t out_index = 0;

    cistring previous;
    for(auto it = std::find_if(table.begin(), table.end(), matcher); it != table.end();
        ++it, it = std::find_if(it, table.end(), matcher))
    {
        if(it->server() != previous)
        {
            out.length(out.length() + 1);
            previous = it->server();
            out[out_index] = string_dup(previous.c_str());
            ++out_index;
        }
    }

    return result._retn();
}

/**
 *	Command DbGetServerNameList related method
 *	Description: Returns the list of server names found for the wildcard specified.
 *               It returns only the server executable name without instance name as DbGetServerList.
 *
 *	@param argin wildcard for server names.
 *	@returns server names found.
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_server_name_list(const Tango::DevString &in) const
{
    using Record = ServerRecord;

    Wildcard matcher{in == nullptr ? "*" : in};
    const std::vector<Record> &table = m_config_tables.get_table<Record>();

    Tango::DevVarStringArray_var result = new Tango::DevVarStringArray;
    auto &out = result.inout();
    size_t out_index = 0;

    cistring previous;
    for(auto it = std::find_if(table.begin(), table.end(), matcher); it != table.end();
        ++it, it = std::find_if(it, table.end(), matcher))
    {
        cistring_view executable = it->server_executable();
        if(executable != previous)
        {
            out.length(out.length() + 1);
            previous = executable;
            out[out_index] = string_dup(previous.c_str());
            ++out_index;
        }
    }

    return result._retn();
}

/**
 *	Command DbGetDeviceClassList related method
 *	Description: Get Tango classes/device list embedded in a specific device server
 *
 *	@param argin Device server process name
 *	@returns Str[0] = Device name
 *           Str[1] = Tango class
 *           Str[n] = Device name
 *           Str[n + 1] = Tango class
 */
Tango::DevVarStringArray *FileDbDeviceBackend::get_device_class_list(const Tango::DevString &in) const
{
    using Record = ServerRecord;

    const std::vector<Record> &table = m_config_tables.get_table<Record>();

    Tango::DevVarStringArray_var result = new Tango::DevVarStringArray;
    auto &out = result.inout();
    size_t out_index = 0;
    out.length(0);

    cistring_view needle{in};
    auto found = std::lower_bound(table.begin(), table.end(), needle, IdentCompare<2>{});

    for(auto it = found; it != table.end() && ident_equiv(*it, needle, 2); ++it)
    {
        out.length(out.length() + 2 * it->devices.size());
        for(const auto &dev : it->devices)
        {
            out[out_index] = string_dup(dev.c_str());
            out_index++;
            out[out_index] = string_dup(it->device_class());
            out_index++;
        }
    }

    return result._retn();
}

/**
 *	Command DbAddServer related method
 *	Description: Create a device server process entry in database
 *
 *	@param argin Str[0] = Full device server name
 *               Str[1] = Device(s) name
 *               Str[2] = Tango class name
 *               Str[n] = Device name
 *               Str[n + 1] = Tango class name
 */
void FileDbDeviceBackend::add_server(const Tango::DevVarStringArray &in)
{
    if(in.length() < 3)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments. Expecting at least 3, found " << in.length() << ".";
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    for(size_t i = 1; i < in.length(); i += 2)
    {
        std::string_view name = in[i].in();
        size_t index1 = name.find('/');
        size_t index2 = name.find('/', index1 + 1);

        if(index1 == 0 || index1 == std::string_view::npos || index2 == std::string_view::npos)
        {
            std::stringstream ss;
            ss << "Invalid device name \"" << name << "\".";
            TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
        }
    }

    delete_server_no_save(in[0].in());

    std::vector<ServerRecord> to_add;
    for(size_t i = 1; i < in.length(); i += 2)
    {
        auto it = std::find_if(to_add.begin(),
                               to_add.end(),
                               [&in, &i](const auto &record) { return record.device_class() == in[i + 1].in(); });
        if(it != to_add.end())
        {
            it->devices.emplace_back(in[i].in());
            continue;
        }

        to_add.emplace_back();
        cistringstream ss;
        ss << in[0].in() << "/" << in[i + 1].in();
        to_add.back().ident = ss.str();
        to_add.back().devices.emplace_back(in[i].in());
    }

    for(const auto &record : to_add)
    {
        m_config_tables.put_record(record);
        add_devices_from_server(record);
    }

    save_config();
}

/**
 *	Command DbPutProperty related method
 *	Description: Create / Update free object property(ies)
 *
 *	@param argin Str[0] = Object name
 *               Str[1] = Property number
 *               Str[2] = Property name
 *               Str[3] = Property value number
 *               Str[4] = Property value 1
 *               Str[n] = Property value n
 *               ....
 */
void FileDbDeviceBackend::put_property(const Tango::DevVarStringArray &in)
{
    if(in.length() < 5)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments. Expecting at least 5, found " << in.length() << ".";
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    size_t in_index = 2;
    while(in_index < in.length())
    {
        FreeObjectPropertyRecord record;
        cistringstream ss;
        ss << in[0].in() << "/" << in[in_index].in();
        record.ident = ss.str();
        ++in_index;

        size_t count;
        {
            std::stringstream ss{in[in_index].in()};
            ++in_index;
            ss >> count;
        }

        record.values.reserve(count);
        for(size_t i = 0; i < count; ++i)
        {
            record.values.emplace_back(in[in_index]);
            ++in_index;
        }

        m_config_tables.put_record(record);
    }

    save_config();
}

/**
 *	Command DbPutDeviceProperty related method
 *	Description: Create / Update device property(ies)
 *
 *	@param argin Str[0] = Tango device name
 *               Str[1] = Property number
 *               Str[2] = Property name
 *               Str[3] = Property value number
 *               Str[4] = Property value 1
 *               Str[n] = Property value n
 *               ....
 */
void FileDbDeviceBackend::put_device_property(const Tango::DevVarStringArray &in)
{
    if(in.length() < 5)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments. Expecting at least 5, found " << in.length() << ".";
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    size_t in_index = 2;
    while(in_index < in.length())
    {
        DevicePropertyRecord record;
        cistringstream ss;
        ss << in[0].in() << "/" << in[in_index].in();
        record.ident = ss.str();
        ++in_index;

        size_t count;
        {
            std::stringstream ss{in[in_index].in()};
            ++in_index;
            ss >> count;
        }

        record.values.reserve(count);
        for(size_t i = 0; i < count; ++i)
        {
            record.values.emplace_back(in[in_index]);
            ++in_index;
        }

        m_config_tables.put_record(record);
    }

    save_config();
}

/**
 *	Command DbDeleteServer related method
 *	Description: Delete server from the database but dont delete device properties
 *
 *	@param argin Device server name
 */
void FileDbDeviceBackend::delete_server(const Tango::DevString &in)
{
    delete_server_no_save(in);

    save_config();
}

void FileDbDeviceBackend::delete_server_no_save(cistring_view server)
{
    if(server.find('/') == std::string::npos)
    {
        std::stringstream ss;
        ss << "Invalid server name \"" << server << "\" .";
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    m_config_tables.delete_if<ServerRecord>([&server](const auto &record) { return record.server() == server; });
    m_runtime_tables.delete_if<DeviceRecord>([&server](const auto &record) { return record.server == server; });
}

/**
 *	Command DbDeleteDeviceProperty related method
 *	Description: Delete device property(ies)
 *
 *	@param argin Str[0] = Device name
 *               Str[1] = Property name
 *               Str[n] = Property name
 */
void FileDbDeviceBackend::delete_device_property(const Tango::DevVarStringArray &in)
{
    using Record = DevicePropertyRecord;

    if(in.length() < 1)
    {
        std::stringstream ss;
        ss << "Invalid number of arguments passed to FileDbDeviceBackend::get_device_list. Expecting at least 1, "
              "found "
           << in.length();
        TANGO_THROW_EXCEPTION(Tango::API_InvalidArgs, ss.str().c_str());
    }

    for(size_t i = 1; i < in.length(); ++i)
    {
        cistringstream ss;
        ss << in[0].in() << "/" << in[i].in();
        m_config_tables.delete_record<Record>(ss.str());
    }

    save_config();
}

void FileDbDeviceBackend::save_config()
{
    if(!m_save)
    {
        return;
    }

    m_config_tables.save(m_filename.c_str());
}

/**
 *	Command DbGetDeviceInfo related method
 *	Description: Returns info from DbImportDevice and started/stopped dates.
 *
 *	@param argin Device name
 *	@returns Str[0] = Device name
 *           Str[1] = CORBA IOR
 *           Str[2] = Device version
 *           Str[3] = Device Server name
 *           Str[4] = Device Server process host name
 *           Str[5] = Started date (or ? if not set)
 *           Str[6] = Stopped date (or ? if not set)
 *           Str[7] = Device class
 *
 *           Lg[0] = Device exported flag
 *           Lg[1] = Device Server process PID (or -1 if not set)
 */
Tango::DevVarLongStringArray *FileDbDeviceBackend::import_device(const Tango::DevString &in) const
{
    Tango::DevVarLongStringArray_var result = new Tango::DevVarLongStringArray;
    auto &out = result.inout();

    out.svalue.length(8);
    out.lvalue.length(2);

    out.svalue[0] = string_dup(in);

    cistring_view device = in;

    auto &table = m_runtime_tables.get_table<DeviceRecord>();

    auto found = std::lower_bound(table.begin(), table.end(), device, IdentCompare{});

    if(found == table.end() || !ident_equiv(*found, device))
    {
        std::stringstream ss;
        ss << "Device " << in << " not defined.";
        TANGO_THROW_EXCEPTION(Tango::DB_DeviceNotDefined, ss.str());
    }

    out.svalue[1] = string_dup(found->ior.c_str());
    out.svalue[2] = string_dup(found->version.c_str());
    out.svalue[3] = string_dup(found->server.c_str());
    out.svalue[4] = string_dup(found->host.c_str());
    if(found->started)
    {
        out.svalue[5] = time_to_corba_string(*found->started);
    }
    else
    {
        out.svalue[5] = string_dup("?");
    }
    if(found->stopped)
    {
        out.svalue[6] = time_to_corba_string(*found->stopped);
    }
    else
    {
        out.svalue[6] = string_dup("?");
    }
    out.svalue[7] = string_dup(found->device_class.c_str());
    out.lvalue[0] = found->exported;
    out.lvalue[1] = found->pid;

    return result._retn();
}

/**
 *	Command DbExportDevice related method
 *	Description: Export a device to the database
 *
 *	@param argin Str[0] = Device name
 *               Str[1] = CORBA IOR
 *               Str[2] = Device server process host name
 *               Str[3] = Device server process PID or string ``null``
 *               Str[4] = Device server process version
 */
void FileDbDeviceBackend::export_device(const Tango::DevVarStringArray &in)
{
    using Record = DeviceRecord;

    const auto &table = m_runtime_tables.get_table<DeviceRecord>();

    cistring_view device = in[0].in();
    auto found = std::lower_bound(table.begin(), table.end(), device, IdentCompare{});

    if(found == table.end() || !ident_equiv(*found, device))
    {
        std::stringstream ss;
        ss << "Device " << in << " not defined.";
        TANGO_THROW_EXCEPTION(Tango::DB_DeviceNotDefined, ss.str());
    }

    Record record;
    record = *found;

    record.exported = true;
    record.ior = in[1].in();
    record.host = in[2].in();
    if(in[3].in() == cistring_view{"NULL"})
    {
        record.pid = -1;
    }
    else
    {
        std::stringstream ss{in[3].in()};
        ss >> record.pid;
    }
    record.version = in[4].in();
    record.started = std::chrono::system_clock::now();

    m_runtime_tables.put_record(record, found);
}

/**
 *	Command DbUnExportDevice related method
 *	Description: Mark a device as non exported in database
 *
 *	@param argin Device name
 */
void FileDbDeviceBackend::unexport_device(const Tango::DevString &in)
{
    using Record = DeviceRecord;

    const auto &table = m_runtime_tables.get_table<DeviceRecord>();

    cistring_view device = in;
    auto found = std::lower_bound(table.begin(), table.end(), device, IdentCompare{});

    if(found == table.end() || !ident_equiv(*found, device))
    {
        std::stringstream ss;
        ss << "Device " << in << " not defined.";
        TANGO_THROW_EXCEPTION(Tango::DB_DeviceNotDefined, ss.str());
    }

    Record record;
    record = *found;

    record.exported = false;
    record.stopped = std::chrono::system_clock::now();

    m_runtime_tables.put_record(record, found);
}

void FileDbDeviceBackend::unexport_server(const Tango::DevString &in)
{
    const auto &table = m_runtime_tables.get_table<DeviceRecord>();

    for(auto it = table.begin(); it != table.end(); ++it)
    {
        if(it->server == in)
        {
            DeviceRecord updated = *it;
            updated.exported = false;
            updated.stopped = std::chrono::system_clock::now();
            m_runtime_tables.put_record(updated, it);
        }
    }
}

void FileDbDeviceBackend::export_self(std::string_view class_name,
                                      std::string_view name,
                                      std::string_view dserver_name,
                                      std::string_view ior,
                                      std::string_view dserver_ior,
                                      std::string_view host,
                                      int pid,
                                      std::string_view version)
{
    TANGO_ASSERT(dserver_name.find("dserver/") == 0);
    constexpr const size_t prefix_len = 8;
    cistring server{dserver_name.data() + prefix_len, dserver_name.size() - prefix_len};

    // Add server records
    {
        cistringstream ss;

        ServerRecord record;
        ss << server << "/" << cistring_view{class_name.data(), class_name.size()};
        record.ident = ss.str();
        ss = {};
        record.devices.emplace_back(name);
        m_config_tables.put_record(record);

        ss << server << "/DServer";
        record.ident = ss.str();
        record.devices.clear();
        record.devices.emplace_back(dserver_name);
        m_config_tables.put_record(record);

        save_config();
    }

    // Add device records
    {
        DeviceRecord record;
        record.ident = cistring{name.data(), name.size()};
        record.device_class = cistring{class_name.data(), class_name.size()};
        record.server = server;
        record.exported = true;
        record.ior = ior;
        record.host = host;
        record.pid = pid;
        record.version = version;
        record.started = std::chrono::system_clock::now();
        m_runtime_tables.put_record(record);

        record.ident = cistring{dserver_name.data(), dserver_name.size()};
        record.device_class = "DServer";
        record.ior = dserver_ior;
        m_runtime_tables.put_record(record);
    }
}

} // namespace FileDb
