#include "filedb/database_tables.h"

#include <tango/tango.h>

#include <ostream>
#include <iomanip>

namespace FileDb
{

cistring_view Record::view_ident_parts(size_t index, size_t part_count) const
{
    size_t start = 0;
    size_t part_end = ident.find('/');
    while(index > 0 && part_end != cistring::npos && part_end != ident.size() - 1)
    {
        start = part_end + 1;
        part_end = ident.find('/', start);
        --index;
    }

    if(index != 0)
    {
        return "";
    }

    size_t end = part_end;
    while(part_count > 1 && end != cistring::npos)
    {
        end = ident.find('/', end + 1);
        --part_count;
    }

    if(end == cistring::npos)
    {
        return cistring_view{ident}.substr(start);
    }
    else
    {
        return cistring_view{ident}.substr(start, end - start);
    }
}

std::ostream &operator<<(std::ostream &os, const ServerRecord &record)
{
    os << record.server() << "/DEVICE/" << record.device_class() << ": ";

    bool first = true;
    for(const auto &device : record.devices)
    {
        if(first)
        {
            first = false;
        }
        else
        {
            os << ", ";
        }

        os << device;
    }

    return os;
}

namespace
{
void print_values(std::ostream &os, const std::vector<std::string> &values)
{
    bool first = true;
    for(const auto &value : values)
    {
        if(first)
        {
            first = false;
        }
        else
        {
            os << ", ";
        }

        // TODO: Split over multiple lines if too long?
        // TODO: Only quote if required?
        // TODO: Escape quotes when quoting
        os << std::quoted(value);
    }
}
} // namespace

std::ostream &operator<<(std::ostream &os, const DevicePropertyRecord &record)
{
    os << record.device() << "->" << record.property() << ": ";
    print_values(os, record.values);

    return os;
}

std::ostream &operator<<(std::ostream &os, const ClassPropertyRecord &record)
{
    os << "CLASS/" << record.device_class() << "->" << record.property() << ": ";
    print_values(os, record.values);

    return os;
}

std::ostream &operator<<(std::ostream &os, const DeviceAttributePropertyRecord &record)
{
    os << record.device() << "/" << record.attribute() << "->" << record.property() << ": ";
    print_values(os, record.values);

    return os;
}

std::ostream &operator<<(std::ostream &os, const ClassAttributePropertyRecord &record)
{
    os << "CLASS/" << record.device_class() << "/" << record.attribute() << "->" << record.property() << ": ";
    print_values(os, record.values);

    return os;
}

std::ostream &operator<<(std::ostream &os, const FreeObjectPropertyRecord &record)
{
    os << "FREE/" << record.object() << "->" << record.property() << ": ";
    print_values(os, record.values);

    return os;
}

// TODO(tri): Move this to a Catch2 string maker and build similar string makers for
// the other records.  Maybe using operator<< for the formatting of the filedb
// file is not the best idea.
std::ostream &operator<<(std::ostream &os, const DeviceRecord &record)
{
    os << "DeviceRecord {";
    os << "\n\tindent: " << record.ident;
    os << "\n\tserver: " << record.server;
    os << "\n\tdevice_class: " << record.device_class;
    os << "\n\texported: " << std::boolalpha << record.exported;
    os << "\n\tior: " << record.ior;
    os << "\n\thost: " << record.host;
    os << "\n\tpid: " << record.pid;
    os << "\n\tversion: " << record.version;
    os << "\n\tstarted: " << (record.started.has_value() ? "SOMETIME" : "?");
    os << "\n\tstopped: " << (record.stopped.has_value() ? "SOMETIME" : "?");
    os << "}";

    return os;
}

std::ostream &operator<<(std::ostream &os, const DbConfigTables &table_set)
{
    table_set.for_each_table(
        [&os](const auto &table)
        {
            for(const auto &record : table)
            {
                os << record << "\n";
            }
        });

    return os;
}

std::ostream &operator<<(std::ostream &os, const cistring &istr)
{
    os << istr.c_str();
    return os;
}

std::ostream &operator<<(std::ostream &os, const cistring_view &istr)
{
    os << std::string_view(istr.data(), istr.size());
    return os;
}

bool Wildcard::operator()(cistring_view s) const
{
    size_t glob_offset = 0;

    enum Kind
    {
        MatchTotal,
        MatchStart,
        MatchMid,
        MatchEnd,
        None,
    };

    struct Part
    {
        Kind kind;
        cistring_view literal;
    };

    auto next_part = [this, &glob_offset]()
    {
        auto make_part = [this](Kind k, size_t s, size_t e)
        { return Part{k, cistring_view{m_glob.data(), m_glob.size()}.substr(s, e - s)}; };

        if(glob_offset > 0 && glob_offset == m_glob.size())
        {
            return Part{None, ""};
        }

        // Doesn't start with a *, so we want to match the substring at the start.
        if(glob_offset == 0 && m_glob[0] != k_glob_char)
        {
            size_t end = m_glob.find(k_glob_char, glob_offset);
            if(end == cistring::npos)
            {
                return Part{MatchTotal, cistring_view{m_glob.data(), m_glob.size()}};
            }

            size_t start = glob_offset;
            glob_offset = end;
            return make_part(MatchStart, start, end);
        }

        TANGO_ASSERT(m_glob[glob_offset] == k_glob_char);

        // Ends in a *, don't need to match anything else.
        if(glob_offset + 1 == m_glob.size())
        {
            return Part{None, ""};
        }

        glob_offset = glob_offset + 1;

        size_t end = m_glob.find(k_glob_char, glob_offset);
        if(end == cistring::npos)
        {
            size_t start = glob_offset;
            glob_offset = m_glob.size();
            return make_part(MatchEnd, start, end);
        }

        size_t start = glob_offset;
        glob_offset = end;
        return make_part(MatchMid, start, end);
    };

    size_t start = 0;
    for(Part part = next_part(); part.kind != None; part = next_part())
    {
        if(part.kind == MatchTotal)
        {
            return part.literal == s;
        }

        size_t found = s.find(part.literal, start);

        if(part.kind == MatchStart && found != 0)
        {
            return false;
        }

        if((part.kind == MatchMid || part.kind == MatchEnd) && found == cistring_view::npos)
        {
            return false;
        }

        if(part.kind == MatchEnd && found + part.literal.size() != s.size())
        {
            return false;
        }

        start = found + part.literal.size();
    }

    return true;
}

} // namespace FileDb
