#pragma once

#include "filedb/cistring.h"

#include <algorithm>
#include <chrono>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <type_traits>
#include <vector>

namespace FileDb
{

struct Record
{
    cistring ident;

    cistring_view view_ident_parts(size_t index, size_t part_count = 1) const;
};

struct ServerRecord : Record
{
    std::vector<std::string> devices;

    cistring_view server() const
    {
        return view_ident_parts(0, 2);
    }

    cistring_view server_executable() const
    {
        return view_ident_parts(0);
    }

    cistring_view server_instance() const
    {
        return view_ident_parts(1);
    }

    cistring_view device_class() const
    {
        return view_ident_parts(2);
    }

    static constexpr const size_t ident_count = 3;
};

std::ostream &operator<<(std::ostream &os, const ServerRecord &record);

struct DevicePropertyRecord : Record
{
    std::vector<std::string> values;

    cistring_view device() const
    {
        return view_ident_parts(0, 3);
    }

    cistring_view domain() const
    {
        return view_ident_parts(0);
    }

    cistring_view family() const
    {
        return view_ident_parts(1);
    }

    cistring_view member() const
    {
        return view_ident_parts(2);
    }

    cistring_view property() const
    {
        return view_ident_parts(3);
    }

    static constexpr const size_t ident_count = 4;
};

std::ostream &operator<<(std::ostream &os, const DevicePropertyRecord &record);

struct ClassPropertyRecord : Record
{
    std::vector<std::string> values;

    cistring_view device_class() const
    {
        return view_ident_parts(0);
    }

    cistring_view property() const
    {
        return view_ident_parts(1);
    }

    static constexpr const size_t ident_count = 2;
};

std::ostream &operator<<(std::ostream &os, const ClassPropertyRecord &record);

struct DeviceAttributePropertyRecord : Record
{
    std::vector<std::string> values;

    cistring_view device() const
    {
        return view_ident_parts(0, 3);
    }

    cistring_view domain() const
    {
        return view_ident_parts(0);
    }

    cistring_view family() const
    {
        return view_ident_parts(1);
    }

    cistring_view member() const
    {
        return view_ident_parts(2);
    }

    cistring_view attribute() const
    {
        return view_ident_parts(3);
    }

    cistring_view property() const
    {
        return view_ident_parts(4);
    }

    static constexpr const size_t ident_count = 5;
};

std::ostream &operator<<(std::ostream &os, const DeviceAttributePropertyRecord &record);

struct ClassAttributePropertyRecord : Record
{
    std::vector<std::string> values;

    cistring_view device_class() const
    {
        return view_ident_parts(0);
    }

    cistring_view attribute() const
    {
        return view_ident_parts(1);
    }

    cistring_view property() const
    {
        return view_ident_parts(2);
    }

    static constexpr const size_t ident_count = 3;
};

std::ostream &operator<<(std::ostream &os, const ClassAttributePropertyRecord &record);

struct FreeObjectPropertyRecord : Record
{
    std::vector<std::string> values;

    cistring_view object() const
    {
        return view_ident_parts(0);
    }

    cistring_view property() const
    {
        return view_ident_parts(1);
    }

    static constexpr const size_t ident_count = 2;
};

std::ostream &operator<<(std::ostream &os, const FreeObjectPropertyRecord &record);

struct DeviceRecord : Record
{
    using TimePoint = std::chrono::system_clock::time_point;

    cistring server;
    cistring device_class;
    bool exported = false;
    std::string ior = "nada";
    std::string host = "nada";
    int pid = -1;
    std::string version = "nada";
    std::optional<TimePoint> started = std::nullopt;
    std::optional<TimePoint> stopped = std::nullopt;

    cistring_view device() const
    {
        return view_ident_parts(0, 3);
    }

    cistring_view domain() const
    {
        return view_ident_parts(0);
    }

    cistring_view family() const
    {
        return view_ident_parts(1);
    }

    cistring_view member() const
    {
        return view_ident_parts(2);
    }

    static constexpr const size_t ident_count = 3;
};

std::ostream &operator<<(std::ostream &os, const DeviceRecord &record);

template <typename T>
bool ident_compare(const T &record, cistring_view rhs, size_t n = T::ident_count)
{
    cistring_view lhs = record.view_ident_parts(0, n);
    return lhs < rhs;
}

template <typename T>
bool ident_compare(cistring_view lhs, const T &record, size_t n = T::ident_count)
{
    cistring_view rhs = record.view_ident_parts(0, n);
    return lhs < rhs;
}

template <typename T>
bool ident_compare(const T &a, const T &b, size_t n = T::ident_count)
{
    cistring_view lhs = a.view_ident_parts(0, n);
    cistring_view rhs = b.view_ident_parts(0, n);
    return lhs < rhs;
}

template <typename T>
bool ident_equiv(const T &record, cistring_view rhs, size_t n = T::ident_count)
{
    cistring_view lhs = record.view_ident_parts(0, n);
    return lhs == rhs;
}

template <typename T>
bool ident_equiv(cistring_view lhs, const T &record, size_t n = T::ident_count)
{
    cistring_view rhs = record.view_ident_parts(0, n);
    return lhs == rhs;
}

template <typename T>
bool ident_equiv(const T &a, const T &b, size_t n = T::ident_count)
{
    cistring_view lhs = a.view_ident_parts(0, n);
    cistring_view rhs = b.view_ident_parts(0, n);
    return lhs == rhs;
}

template <size_t N = 0>
struct IdentCompare
{
    template <typename T, typename U>
    bool operator()(const T &lhs, const U &rhs)
    {
        if constexpr(N == 0)
        {
            return ident_compare(lhs, rhs);
        }

        return ident_compare(lhs, rhs, N);
    }
};

class Wildcard
{
  public:
    static constexpr const char k_glob_char = '*';

    explicit Wildcard(const std::string &glob) :
        m_glob{glob}
    {
    }

    explicit Wildcard(std::string &&glob) :
        m_glob{std::move(glob)}
    {
    }

    explicit Wildcard(const char *glob) :
        m_glob{glob}
    {
    }

    bool operator()(cistring_view s) const;

    template <typename Record>
    std::enable_if_t<!std::is_convertible_v<Record, cistring_view>, bool> operator()(const Record &record) const
    {
        return operator()(record.ident);
    }

  private:
    std::string m_glob;
};

namespace detail
{
template <typename... Records>
class TableSet
{
  public:
    template <typename Fn>
    void for_each_table(Fn fn) const
    {
        std::apply([&fn](const auto &...tables) { (fn(tables), ...); }, m_tables);
    }

    // Tables are sorted by ident
    template <typename Record>
    std::enable_if_t<(std::is_same_v<Record, Records> || ...), const std::vector<Record>> &get_table() const
    {
        return std::get<std::vector<Record>>(const_cast<TableSet *>(this)->m_tables);
    }

    // Replaces the values if already found.
    template <typename Record>
    std::enable_if_t<(std::is_same_v<Record, Records> || ...), void> put_record(const Record &record)
    {
        auto &table = std::get<std::vector<Record>>(m_tables);
        auto it = std::lower_bound(table.begin(), table.end(), record, IdentCompare{});

        if(it != table.end() && ident_equiv(*it, record))
        {
            *it = record;
        }
        else
        {
            table.emplace(it, record);
        }
    }

    template <typename Record>
    std::enable_if_t<(std::is_same_v<Record, Records> || ...), void>
        put_record(const Record &record, typename std::vector<Record>::const_iterator hint)
    {
        auto &table = std::get<std::vector<Record>>(m_tables);

        size_t index = (hint - table.cbegin());

        if(index < table.size() && ident_equiv(table[index], record))
        {
            table[index] = record;
        }
        else
        {
            put_record(record);
        }
    }

    // Does nothing if not found.
    template <typename Record>
    std::enable_if_t<(std::is_same_v<Record, Records> || ...), void> delete_record(cistring_view ident)
    {
        auto &table = std::get<std::vector<Record>>(m_tables);
        auto it = std::lower_bound(table.begin(), table.end(), ident, IdentCompare{});

        if(it != table.end() && ident_equiv(*it, ident))
        {
            table.erase(it);
        }
    }

    template <typename Record, typename Pred>
    std::enable_if_t<(std::is_same_v<Record, Records> || ...), void> delete_if(Pred pred)
    {
        auto &table = std::get<std::vector<Record>>(m_tables);
        table.erase(std::remove_if(table.begin(), table.end(), pred), table.end());
    }

  protected:
    template <typename Fn>
    void for_each_table(Fn fn)
    {
        std::apply([&fn](auto &...tables) { (fn(tables), ...); }, m_tables);
    }

    std::tuple<std::vector<Records>...> m_tables;
};

}; // namespace detail

class DbConfigTables : public detail::TableSet<ServerRecord,
                                               DevicePropertyRecord,
                                               ClassPropertyRecord,
                                               FreeObjectPropertyRecord,
                                               DeviceAttributePropertyRecord,
                                               ClassAttributePropertyRecord>
{
  public:
    void load(const char *filename);
    void save(const char *filename) const;
};

std::ostream &operator<<(std::ostream &os, const DbConfigTables &table);

using DbRuntimeTables = detail::TableSet<DeviceRecord>;

} // namespace FileDb
