#include "filedb/parser.h"
#include "filedb/internal/parser_types.h"
#include "filedb/database_tables.h"

#include <tango/tango.h>

#include <deque>
#include <algorithm>
#include <sstream>

namespace FileDb
{

namespace detail
{

namespace
{

std::ostream &operator<<(std::ostream &os, const Token &token)
{
    using Kind = Token::Kind;
    switch(token.kind)
    {
    case Kind::STRING:
        os << "string '" << token.str << "'";
        break;
    case Kind::COMMA:
        os << "','";
        break;
    case Kind::COLON:
        os << "':'";
        break;
    case Kind::SLASH:
        os << "'/'";
        break;
    case Kind::ARROW:
        os << "'->'";
        break;
    case Kind::END_OF_FILE:
        os << "end of file";
        break;
    case Kind::ERROR:
        os << "Error: " << token.str;
        break;
    }

    return os;
}
} // namespace

void Tokenizer::unquoted_is_identifier() noexcept
{
    TANGO_ASSERT(m_state == State::TOKEN_BEGIN || m_state == State::DONE);
    m_unquoted_is_identifier = true;
}

void Tokenizer::unquoted_is_string() noexcept
{
    TANGO_ASSERT(m_state == State::TOKEN_BEGIN || m_state == State::DONE);
    m_unquoted_is_identifier = false;
}

Token Tokenizer::make_token(Token::Kind kind) noexcept
{
    m_current_token = Token{kind, "", m_col, m_lineno};
    return m_current_token;
}

template <typename... Args>
Token Tokenizer::make_error(Args &&...args) noexcept
{
    std::stringstream ss;
    (ss << ... << std::forward<Args &&>(args));
    m_state = State::DONE;
    m_current_token = Token{Token::Kind::ERROR, ss.str(), m_col, m_lineno};
    return m_current_token;
}

char Tokenizer::peek(size_t index) const noexcept
{
    TANGO_ASSERT(index < m_lookahead.size());
    return m_lookahead[index];
}

void Tokenizer::consume() noexcept
{
    m_lookahead.pop_front();
}

bool Tokenizer::is_char(char c, size_t index) const noexcept
{
    return peek(index) == c;
}

bool Tokenizer::is_sequence(std::string_view seq, size_t index) const noexcept
{
    TANGO_ASSERT(index + seq.size() - 1 < m_lookahead.size());
    auto begin = m_lookahead.begin() + index;
    auto end = begin + seq.size();
    return std::equal(seq.begin(), seq.end(), begin, end);
}

bool Tokenizer::is_whitespace(std::size_t index) const noexcept
{
    // Definition taken from cppTango 10.0.0 implementation of the FileDb
    char c = peek(index);
    return c > 0 && c <= 32;
}

bool Tokenizer::is_eof(std::size_t index) const noexcept
{
    return peek(index) == '\0';
}

bool Tokenizer::is_end_of_unquoted() const noexcept
{
    if(m_unquoted_is_identifier)
    {
        return is_whitespace(1) || is_eof(1) || is_char('/', 1) || is_char(',', 1) || is_sequence("->", 1) ||
               is_char(':', 1) || is_sequence("\\\n", 1);
    }
    else
    {
        return is_whitespace(1) || is_eof(1) || is_char(',', 1) || is_sequence("\\\n", 1);
    }
}

std::optional<Token> Tokenizer::feed(char c) noexcept
{
    std::optional<Token> result = std::nullopt;

    if(m_state == State::DONE)
    {
        result = m_current_token;
        return result;
    }

    // TODO: Worry about utf8 here?

    m_lookahead.push_back(c);
    if(m_lookahead.size() < 3)
    {
        return result;
    }

    switch(m_state)
    {
    case State::TOKEN_BEGIN:
    {
        // Skip line comments
        if(is_char('#'))
        {
            m_state = State::LINE_COMMENT;
            break;
        }

        // Skip spaces
        if(is_whitespace())
        {
            break;
        }

        // Skip escaped new line
        if(is_sequence("\\\n"))
        {
            consume();
            break;
        }

        if(is_eof())
        {
            m_state = State::DONE;
            result = make_token(Token::Kind::END_OF_FILE);
            break;
        }
        if(is_char('/'))
        {
            result = make_token(Token::Kind::SLASH);
            m_state = State::TOKEN_BEGIN;
            break;
        }
        if(is_char(':'))
        {
            result = make_token(Token::Kind::COLON);
            m_state = State::TOKEN_BEGIN;
            break;
        }
        if(is_char(','))
        {
            result = make_token(Token::Kind::COMMA);
            m_state = State::TOKEN_BEGIN;
            break;
        }
        if(is_sequence("->"))
        {
            result = make_token(Token::Kind::ARROW);
            consume();
            m_state = State::TOKEN_BEGIN;
            break;
        }
        if(is_char('\"'))
        {
            make_token(Token::Kind::STRING);
            m_state = State::QUOTED_STRING;
            break;
        }

        // We don't want to consume the character here as it is part of the
        // string/identifier, so we fallthrough to UNQUOTED_STRING to process it
        make_token(Token::Kind::STRING);
        m_state = State::UNQUOTED_STRING;
    }
        [[fallthrough]];
    case State::UNQUOTED_STRING:
    {
        m_current_token.str += peek();

        if(is_end_of_unquoted())
        {
            result = m_current_token;
            m_state = State::TOKEN_BEGIN;
            break;
        }
    }
    break;
    case State::QUOTED_STRING:
    {
        if(is_char('\n'))
        {
            result = make_error("Unexpected new line, missing closing \"");
        }

        if(is_eof())
        {
            result = make_error("Unexpected end of file, missing closing \"");
        }

        if(is_sequence("\\\""))
        {
            consume();
            m_current_token.str += peek();
            break;
        }

        if(is_char('\"'))
        {
            result = m_current_token;
            m_state = State::TOKEN_BEGIN;
            break;
        }

        m_current_token.str += peek();
    }
    break;
    case State::LINE_COMMENT:
    {
        if(is_char('\n'))
        {
            m_state = State::TOKEN_BEGIN;
        }
    }
    break;
    default:
        TANGO_ASSERT(false);
    }

    if(is_char('\n'))
    {
        m_lineno++;
        m_col = 1;
    }
    else
    {
        m_col++;
    }

    consume();

    return result;
}

const Token &Parser::peek(size_t index)
{
    TANGO_ASSERT(index < m_lookahead.size());

    return m_lookahead[index];
}

void Parser::next_token()
{
    using Kind = Token::Kind;
    if(!m_lookahead.empty() && (peek().kind == Kind::COLON || peek().kind == Kind::COMMA))
    {
        m_tokenizer.unquoted_is_string();
    }
    else
    {
        m_tokenizer.unquoted_is_identifier();
    }

    std::optional<Token> maybe_token = std::nullopt;
    while(!maybe_token)
    {
        char c;
        errno = 0;
        m_stream.get(c);

        if(m_stream.bad())
        {
            std::stringstream ss;
            ss << "Failed to read character";
            if(errno != 0)
            {
                ss << ": " << std::strerror(errno);
            }
            ss << ".";
            TANGO_THROW_EXCEPTION(FILEDB_IoError, ss.str());
        }

        if(m_stream.eof())
        {
            c = '\0';
        }
        maybe_token = m_tokenizer.feed(c);
    }

    if(maybe_token->kind == Token::Kind::ERROR)
    {
        throw_at_token(FILEDB_ScanError, *maybe_token, maybe_token->str);
    }

    m_lookahead.push_back(*maybe_token);
}

void Parser::consume()
{
    m_lookahead.pop_front();
}

template <typename... Args>
[[noreturn]] void Parser::throw_at_token(const char *reason, size_t offset, const Token &token, Args &&...args)
{
    std::stringstream ss;
    ss << m_filename << ":" << token.lineno << ":" << token.col + offset << ": ";
    (ss << ... << std::forward<Args &&>(args));
    TANGO_THROW_EXCEPTION(reason, ss.str());
}

bool Parser::is_end_of_record()
{
    return peek(1).kind != Token::Kind::COMMA;
}

Parser::RecordEvent Parser::next_event()
{
    // The IDENTIFIER/IDENTIFIER[/IDENTIFIER[/IDENTIFIER]] before the : or ->.
    std::vector<cistring> identifier;

    // The IDENTIFIER after the ->
    std::optional<cistring> property;

    // The STRINGs after the :
    std::vector<std::string> values;

    enum class State
    {
        IDENTIFIER,
        IDENTIFIER_SEP,
        PROPERTY_NAME,
        PROPERTY_NAME_SEP,
        VALUE,
        VALUE_SEP,
        DONE
    };

    State state = State::IDENTIFIER;
    do
    {
        using Kind = Token::Kind;
        // We can only consume at most a single token on each iteration of the
        // loop because next_token requires us to know the current token to
        // decide how to scan unquoted strings.
        next_token();
        if(m_lookahead.size() < 2)
        {
            continue;
        }

        const auto &token = peek();

        if(token.kind == Kind::END_OF_FILE)
        {
            return RecordEvent{EndOfFile{}};
        }

        switch(state)
        {
        case State::IDENTIFIER:
        {
            if(token.kind != Kind::STRING)
            {
                throw_at_token(FILEDB_ParseError, token, "Unexpected ", token, ", expecting STRING.");
            }

            identifier.emplace_back(token.str.c_str(), token.str.size());
            state = State::IDENTIFIER_SEP;
        }
        break;
        case State::IDENTIFIER_SEP:
        {
            switch(token.kind)
            {
            case Kind::SLASH:
                if(identifier.size() == 4)
                {
                    throw_at_token(
                        FILEDB_ParseError, token, "Unexpected ", token, ". Too many preceeding parts to name.");
                }
                state = State::IDENTIFIER;
                break;
            case Kind::ARROW:
                if(identifier.size() == 2 && !(identifier[0] == "class" || identifier[0] == "free"))
                {
                    throw_at_token(FILEDB_ParseError,
                                   token,
                                   "Unexpected ",
                                   token,
                                   ". Not enough parts to name for none-CLASS/FREE record.");
                }
                else if(identifier.size() == 1)
                {
                    throw_at_token(
                        FILEDB_ParseError, token, "Unexpected ", token, ". Not enough preceeding parts to name.");
                }
                state = State::PROPERTY_NAME;
                break;
            case Kind::COLON:
                if(identifier.size() != 4 || identifier[2] != "device")
                {
                    throw_at_token(FILEDB_ParseError, token, "Unexpected ", token, " for none-DEVICE record.");
                }
                state = State::VALUE;
                break;
            default:
                // TODO: Improve this to only include expecting things we actual expect.
                throw_at_token(FILEDB_ParseError, token, "Unexpected ", token, ". Expecting '/', ':' or '->'.");
            }
        }
        break;
        case State::PROPERTY_NAME:
        {
            if(token.kind != Kind::STRING)
            {
                throw_at_token(FILEDB_ParseError, token, "Unexpected ", token, ". Expecting property name identifier.");
            }

            property.emplace(token.str.c_str(), token.str.size());
            state = State::PROPERTY_NAME_SEP;
        }
        break;
        case State::PROPERTY_NAME_SEP:
        {
            if(token.kind != Kind::COLON)
            {
                throw_at_token(FILEDB_ParseError, token, "Unexpected ", token, ". Expecting ':'.");
            }

            state = State::VALUE;
        }
        break;
        case State::VALUE:
        {
            if(token.kind != Kind::STRING)
            {
                throw_at_token(FILEDB_ParseError, token, "Unexpected ", token, ". Expecting value string.");
            }

            values.push_back(token.str);

            if(is_end_of_record())
            {
                state = State::DONE;
                break;
            }

            state = State::VALUE_SEP;
        }
        break;
        case State::VALUE_SEP:
        {
            TANGO_ASSERT(token.kind == Kind::COMMA);

            state = State::VALUE;
        }
        break;
        case State::DONE:
            TANGO_ASSERT(false);
        }

        consume();
    } while(state != State::DONE);

    // Device definition
    // e.g. exe/inst/DEVICE/class: domain1/family1/member1, domain2/family2/member2
    if(!property)
    {
        TANGO_ASSERT(identifier.size() == 4);
        TANGO_ASSERT(identifier[2] == "device");

        ServerRecord record;
        cistringstream ss;
        ss << identifier[0] << "/" << identifier[1] << "/" << identifier[3];
        record.ident = ss.str();
        record.devices = values;

        return RecordEvent{record};
    }

    // class property
    // e.g. CLASS/device_class->property: value1, value2, value3
    if(identifier.size() == 2 && identifier[0] == "class")
    {
        TANGO_ASSERT(property.has_value());

        ClassPropertyRecord record;
        cistringstream ss;
        ss << identifier[1] << "/" << *property;
        record.ident = ss.str();
        record.values = values;

        return RecordEvent{record};
    }

    // free object property
    // e.g. FREE/object->property: value1, value2, value3
    if(identifier.size() == 2 && identifier[0] == "free")
    {
        TANGO_ASSERT(property.has_value());

        FreeObjectPropertyRecord record;
        cistringstream ss;
        ss << identifier[1] << "/" << *property;
        record.ident = ss.str();
        record.values = values;

        return RecordEvent{record};
    }

    // Class Attribute property
    // e.g. CLASS/device_class/attribute->property: value1, value2, value3
    if(identifier.size() == 3 && identifier[0] == "class")
    {
        TANGO_ASSERT(property.has_value());

        ClassAttributePropertyRecord record;
        cistringstream ss;
        ss << identifier[1] << "/" << identifier[2] << "/" << *property;
        record.ident = ss.str();
        record.values = values;

        return RecordEvent{record};
    }

    // Device property
    // e.g. domain/family/member->property: value1, value2, value3
    if(identifier.size() == 3)
    {
        TANGO_ASSERT(property.has_value());

        DevicePropertyRecord record;
        cistringstream ss;
        ss << identifier[0] << "/" << identifier[1] << "/" << identifier[2] << "/" << *property;
        record.ident = ss.str();
        record.values = values;

        return RecordEvent{record};
    }

    // Attribute property
    // e.g. domain/family/member/attribute->property: value1, value2, value3
    if(identifier.size() == 4)
    {
        TANGO_ASSERT(property.has_value());

        DeviceAttributePropertyRecord record;
        cistringstream ss;
        ss << identifier[0] << "/" << identifier[1] << "/" << identifier[2] << "/" << identifier[3] << "/" << *property;
        record.ident = ss.str();
        record.values = values;

        return RecordEvent{record};
    }

    TANGO_ASSERT(false);
}

} // namespace detail

void DbConfigTables::load(const char *path)
{
    using namespace detail;

    errno = 0;
    std::ifstream in{path};
    if(!in.is_open())
    {
        std::stringstream ss;
        ss << "Failed to open " << std::quoted(path);
        if(errno != 0)
        {
            ss << ": " << std::strerror(errno);
        }
        ss << ".";
        TANGO_THROW_EXCEPTION(FILEDB_IoError, ss.str());
    }

    Parser parser{in, path};

    while(true)
    {
        Parser::RecordEvent event = parser.next_event();

        if(std::holds_alternative<Parser::EndOfFile>(event))
        {
            break;
        }

        std::visit(
            [this](auto &&record)
            {
                using T = std::decay_t<decltype(record)>;
                if constexpr(!std::is_same_v<T, Parser::EndOfFile>)
                {
                    std::get<std::vector<T>>(m_tables).push_back(record);
                }
            },
            event);
    }

    for_each_table([](auto &table) { std::sort(table.begin(), table.end(), IdentCompare{}); });
}

void DbConfigTables::save(const char *path) const
{
    errno = 0;
    std::ofstream out{path};
    if(!out.is_open())
    {
        std::stringstream ss;
        ss << "Failed to open " << std::quoted(path);
        if(errno != 0)
        {
            ss << ": " << std::strerror(errno);
        }
        ss << ".";
        TANGO_THROW_EXCEPTION(FILEDB_IoError, ss.str());
    }

    out << *this;
}

} // namespace FileDb
