/*
 * Copyright (c) 2016 MariaDB Corporation Ab
 *
 * Use of this software is governed by the Business Source License included
 * in the LICENSE.TXT file and at www.mariadb.com/bsl11.
 *
 * Change Date: 2020-01-01
 *
 * On the date above, in accordance with the Business Source License, use
 * of this software will be governed by version 2 or later of the General
 * Public License.
 */

#include "testreader.hh"
#include <algorithm>
#include <map>
#include <iostream>

using std::istream;
using std::string;
using std::map;

namespace
{

enum skip_action_t
{
    SKIP_NOTHING,        // Skip nothing.
    SKIP_BLOCK,          // Skip until the end of next { ... }
    SKIP_DELIMITER,      // Skip the new delimiter.
    SKIP_LINE,           // Skip current line.
    SKIP_NEXT_STATEMENT, // Skip statement starting on line following this line.
    SKIP_STATEMENT,      // Skip statment starting on this line.
    SKIP_TERMINATE,      // Cannot handle this, terminate.
};

typedef std::map<std::string, skip_action_t> KeywordActionMapping;

static KeywordActionMapping mtl_keywords;
static KeywordActionMapping plsql_keywords;

void init_keywords()
{
    struct Keyword
    {
        const char* z_keyword;
        skip_action_t action;
    };

    static const Keyword MTL_KEYWORDS[] =
    {
        { "append_file",                SKIP_LINE },
        { "cat_file",                   SKIP_LINE },
        { "change_user",                SKIP_LINE },
        { "character_set",              SKIP_LINE },
        { "chmod",                      SKIP_LINE },
        { "connect",                    SKIP_LINE },
        { "connection",                 SKIP_LINE },
        { "copy_file",                  SKIP_LINE },
        { "dec",                        SKIP_LINE },
        { "delimiter",                  SKIP_DELIMITER },
        { "die",                        SKIP_LINE },
        { "diff_files",                 SKIP_LINE },
        { "dirty_close",                SKIP_LINE },
        { "disable_abort_on_error",     SKIP_LINE },
        { "disable_connect_log",        SKIP_LINE },
        { "disable_info",               SKIP_LINE },
        { "disable_metadata",           SKIP_LINE },
        { "disable_parsing",            SKIP_LINE },
        { "disable_ps_protocol",        SKIP_LINE },
        { "disable_query_log",          SKIP_LINE },
        { "disable_reconnect",          SKIP_LINE },
        { "disable_result_log",         SKIP_LINE },
        { "disable_rpl_parse",          SKIP_LINE },
        { "disable_session_track_info", SKIP_LINE },
        { "disable_warnings",           SKIP_LINE },
        { "disconnect",                 SKIP_LINE },
        { "echo",                       SKIP_LINE },
        { "enable_abort_on_error",      SKIP_LINE },
        { "enable_connect_log",         SKIP_LINE },
        { "enable_info",                SKIP_LINE },
        { "enable_metadata",            SKIP_LINE },
        { "enable_parsing",             SKIP_LINE },
        { "enable_ps_protocol",         SKIP_LINE },
        { "enable_query_log",           SKIP_LINE },
        { "enable_reconnect",           SKIP_LINE },
        { "enable_result_log",          SKIP_LINE },
        { "enable_rpl_parse",           SKIP_LINE },
        { "enable_session_track_info",  SKIP_LINE },
        { "enable_warnings",            SKIP_LINE },
        { "end_timer",                  SKIP_LINE },
        { "error",                      SKIP_NEXT_STATEMENT },
        { "eval",                       SKIP_STATEMENT },
        { "exec",                       SKIP_LINE },
        { "file_exists",                SKIP_LINE },
        { "horizontal_results",         SKIP_LINE },
        { "inc",                        SKIP_LINE },
        { "let",                        SKIP_LINE },
        { "let",                        SKIP_LINE },
        { "list_files",                 SKIP_LINE },
        { "list_files_append_file",     SKIP_LINE },
        { "list_files_write_file",      SKIP_LINE },
        { "lowercase_result",           SKIP_LINE },
        { "mkdir",                      SKIP_LINE },
        { "move_file",                  SKIP_LINE },
        { "output",                     SKIP_LINE },
        { "perl",                       SKIP_TERMINATE },
        { "ping",                       SKIP_LINE },
        { "print",                      SKIP_LINE },
        { "query",                      SKIP_LINE },
        { "query_get_value",            SKIP_LINE },
        { "query_horizontal",           SKIP_LINE },
        { "query_vertical",             SKIP_LINE },
        { "real_sleep",                 SKIP_LINE },
        { "reap",                       SKIP_LINE },
        { "remove_file",                SKIP_LINE },
        { "remove_files_wildcard",      SKIP_LINE },
        { "replace_column",             SKIP_LINE },
        { "replace_regex",              SKIP_LINE },
        { "replace_result",             SKIP_LINE },
        { "require",                    SKIP_LINE },
        { "reset_connection",           SKIP_LINE },
        { "result",                     SKIP_LINE },
        { "result_format",              SKIP_LINE },
        { "rmdir",                      SKIP_LINE },
        { "same_master_pos",            SKIP_LINE },
        { "send",                       SKIP_LINE },
        { "send_eval",                  SKIP_LINE },
        { "send_quit",                  SKIP_LINE },
        { "send_shutdown",              SKIP_LINE },
        { "skip",                       SKIP_LINE },
        { "sleep",                      SKIP_LINE },
        { "sorted_result",              SKIP_LINE },
        { "source",                     SKIP_LINE },
        { "start_timer",                SKIP_LINE },
        { "sync_slave_with_master",     SKIP_LINE },
        { "sync_with_master",           SKIP_LINE },
        { "system",                     SKIP_LINE },
        { "vertical_results",           SKIP_LINE },
        { "write_file",                 SKIP_LINE },
    };

    const size_t N_MTL_KEYWORDS = sizeof(MTL_KEYWORDS)/sizeof(MTL_KEYWORDS[0]);

    for (size_t i = 0; i < N_MTL_KEYWORDS; ++i)
    {
        mtl_keywords[MTL_KEYWORDS[i].z_keyword] = MTL_KEYWORDS[i].action;
    }

    static const Keyword PLSQL_KEYWORDS[] =
    {
        { "exit",                       SKIP_LINE },
        { "if",                         SKIP_BLOCK },
        { "while",                      SKIP_BLOCK },
    };

    const size_t N_PLSQL_KEYWORDS = sizeof(PLSQL_KEYWORDS)/sizeof(PLSQL_KEYWORDS[0]);

    for (size_t i = 0; i < N_PLSQL_KEYWORDS; ++i)
    {
        plsql_keywords[PLSQL_KEYWORDS[i].z_keyword] = PLSQL_KEYWORDS[i].action;
    }
}

skip_action_t get_action(const string& keyword, const string& delimiter)
{
    skip_action_t action = SKIP_NOTHING;

    string key(keyword);
    std::transform(key.begin(), key.end(), key.begin(), ::tolower);

    if (key == "delimiter")
    {
        // DELIMITER is directly understood by the parser so it needs to
        // be handled explicitly.
        action = SKIP_DELIMITER;
    }
    else
    {
        KeywordActionMapping::iterator i = mtl_keywords.find(key);

        if (i != mtl_keywords.end())
        {
            action = i->second;
        }
    }

    if ((action == SKIP_NOTHING) && (delimiter == ";"))
    {
        // Some mysqltest keywords, such as "while", "exit" and "if" are also
        // PL/SQL keywords. We assume they can only be used in the former role,
        // if the delimiter is ";".
        KeywordActionMapping::iterator i = plsql_keywords.find(key);

        if (i != plsql_keywords.end())
        {
            action = i->second;
        }
    }

    return action;
}

inline void ltrim(std::string &s)
{
    s.erase(s.begin(), std::find_if(s.begin(), s.end(), std::not1(std::ptr_fun<int, int>(std::isspace))));
}

inline void rtrim(std::string &s)
{
    s.erase(std::find_if(s.rbegin(), s.rend(),
                         std::not1(std::ptr_fun<int, int>(std::isspace))).base(), s.end());
}

void trim(std::string &s)
{
    ltrim(s);
    rtrim(s);
}

}

namespace maxscale
{

TestReader::TestReader(istream& in,
                       size_t   line)
    : m_in(in)
    , m_line(line)
    , m_delimiter(";")
{
    init();
}

TestReader::result_t TestReader::get_statement(std::string& stmt)
{
    bool error = false; // Whether an error has occurred.
    bool found = false; // Whether we have found a statement.
    bool skip = false;  // Whether next statement should be skipped.

    stmt.clear();

    string line;

    while (!error && !found && std::getline(m_in, line))
    {
        trim(line);

        m_line++;

        if (!line.empty() && (line.at(0) != '#'))
        {
            // Ignore comment lines.
            if ((line.substr(0, 3) == "-- ") || (line.substr(0, 1) == "#"))
            {
                continue;
            }

            if (!skip)
            {
                if (line.substr(0, 2) == "--")
                {
                    line = line.substr(2);
                    trim(line);
                }

                string::iterator i = std::find_if(line.begin(), line.end(),
                                                  std::ptr_fun<int,int>(std::isspace));
                string keyword = line.substr(0, i - line.begin());

                skip_action_t action = get_action(keyword, m_delimiter);

                switch (action)
                {
                case SKIP_NOTHING:
                    break;

                case SKIP_BLOCK:
                    skip_block();
                    continue;

                case SKIP_DELIMITER:
                    line = line.substr(i - line.begin());
                    trim(line);
                    if (line.length() > 0)
                    {
                        if (line.length() >= m_delimiter.length())
                        {
                            if (line.substr(line.length() - m_delimiter.length()) == m_delimiter)
                            {
                                m_delimiter = line.substr(0, line.length() - m_delimiter.length());
                            }
                            else
                            {
                                m_delimiter = line;
                            }
                        }
                        else
                        {
                            m_delimiter = line;
                        }
                    }
                    continue;

                case SKIP_LINE:
                    continue;

                case SKIP_NEXT_STATEMENT:
                    skip = true;
                    continue;

                case SKIP_STATEMENT:
                    skip = true;
                    break;

                case SKIP_TERMINATE:
                    MXS_ERROR("Cannot handle line %u: %s", (unsigned)m_line, line.c_str());
                    error = true;
                    break;
                }
            }

            stmt += line;

            // Look for a ';'. If we are dealing with a one line test statment
            // the delimiter will in practice be ';' and if it is a multi-line
            // test statement then the test-script delimiter will be something
            // else than ';' and ';' will be the delimiter used in the multi-line
            // statement.
            string::size_type i = line.find(";");

            if (i != string::npos)
            {
                // Is there a "-- " or "#" after the delimiter?
                if ((line.find("-- ", i) != string::npos) ||
                    (line.find("#", i) != string::npos))
                {
                    // If so, add a newline. Otherwise the rest of the
                    // statement would be included in the comment.
                    stmt += "\n";
                }

                // This is somewhat fragile as a ";", "#" or "-- " inside a
                // string will trigger this behaviour...
            }

            string c;

            if (line.length() >= m_delimiter.length())
            {
                c = line.substr(line.length() - m_delimiter.length());
            }

            if (c == m_delimiter)
            {
                if (c != ";")
                {
                    // If the delimiter was something else but ';' we need to
                    // remove that before giving the line to the classifiers.
                    stmt.erase(stmt.length() - m_delimiter.length());
                }

                if (!skip)
                {
                    found = true;
                }
                else
                {
                    skip = false;
                    stmt.clear();
                }
            }
            else if (!skip)
            {
                stmt += " ";
            }
        }
        else if (line.substr(0, 7) == "--error")
        {
            // Next statement is supposed to fail, no need to check.
            skip = true;
        }
    }

    result_t result;

    if (error)
    {
        result = RESULT_ERROR;
    }
    else if (found)
    {
        result = RESULT_STMT;
    }
    else
    {
        result = RESULT_EOF;
    }

    return result;
}

// static
void TestReader::init()
{
    static bool inited = false;

    if (!inited)
    {
        inited = true;

        init_keywords();
    }
}

void TestReader::skip_block()
{
    int c;

    // Find first '{'
    while (m_in && ((c = m_in.get()) != '{'))
    {
        if (c == '\n')
        {
            ++m_line;
        }
    }

    int n = 1;

    while ((n > 0) && m_in)
    {
        c = m_in.get();

        switch (c)
        {
        case '{':
            ++n;
            break;

        case '}':
            --n;
            break;

        case '\n':
            ++m_line;
            break;

        default:
            ;
        }
    }
}

}
