/*
 * Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is also distributed with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms, as
 * designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have included with MySQL.
 * This program is distributed in the hope that it will be useful,  but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
 * the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 
 */

#include <pcrecpp.h>
#include <thread>
#include <rapidjson/istreamwrapper.h>
#include <fstream>

#include "base/log.h"
#include "base/string_utilities.h"
#include "base/file_utilities.h"

#include "mforms/app.h"
#include "mforms/code_editor.h"

#include "grtdb/db_helpers.h"
#include "workbench/wb_backend_public_interface.h"
#include "wb_sql_editor_help.h"

#include "mysql/mysql-recognition-types.h"
#include "mysql/MySQLRecognizerCommon.h"
#include "mysql/MySQLLexer.h"
#include "mysql/MySQLParser.h"

DEFAULT_LOG_DOMAIN("Context help")

using namespace parsers;
using namespace antlr4;

using namespace help;
using namespace rapidjson;

class HelpContext::Private {
public:
  Private() : lexer(&input), tokens(&lexer), parser(&tokens) {
  }

  ANTLRInputStream input;
  MySQLLexer lexer;
  CommonTokenStream tokens;
  MySQLParser parser;

  ParserRuleContext *parse(const std::string &query) {
    input.load(query);
    lexer.reset();
    lexer.setInputStream(&input);
    tokens.setTokenSource(&lexer);

    parser.reset();
    return parser.query();
  }
};

//----------------- HelpContext ----------------------------------------------------------------------------------------

HelpContext::HelpContext(GrtCharacterSetsRef charsets, const std::string &sqlMode, long serverVersion) {
  _d = new Private();
  std::set<std::string> filteredCharsets;
  for (size_t i = 0; i < charsets->count(); i++)
    filteredCharsets.insert("_" + base::tolower(*charsets[i]->name()));

  if (_d->lexer.serverVersion < 50503) {
    filteredCharsets.erase("_utf8mb4");
    filteredCharsets.erase("_utf16");
    filteredCharsets.erase("_utf32");
  } else {
    filteredCharsets.insert("_utf8mb4");
    filteredCharsets.insert("_utf16");
    filteredCharsets.insert("_utf32");
  }
  _d->lexer.charsets = filteredCharsets;
  _d->lexer.serverVersion = serverVersion;

  _d->lexer.sqlModeFromString(sqlMode);
  _d->parser.sqlMode = _d->lexer.sqlMode;

  _d->parser.serverVersion = serverVersion;
  _d->parser.removeParseListeners();
  _d->parser.removeErrorListeners();
}

//----------------------------------------------------------------------------------------------------------------------

HelpContext::~HelpContext() {
  delete _d;
}

//----------------------------------------------------------------------------------------------------------------------

long HelpContext::serverVersion() const {
  return _d->lexer.serverVersion;
}

//----------------- DbSqlEditorContextHelp -----------------------------------------------------------------------------

DbSqlEditorContextHelp *DbSqlEditorContextHelp::get() {
  static DbSqlEditorContextHelp instance;
  return &instance;
}

//----------------------------------------------------------------------------------------------------------------------

static std::string helpStyleSheetTemplate = "<style>\n"
  "body { color: »textColor«; background-color: »mainBackground«; spacing: 5px; }\n"
  "emphasis {font-style: italic; font-size: 100%; font-weight: 400;}\n"
  "literal { font-family: monospace; background-color: »literalBackground«; color: »textColor«; }\n"
  "literal[role='stmt'] { font-weight: 600; }\n"
  "literal[role='func'] { font-weight: 600; }\n"
  "literal[role='cfunc'] { color: #ba0099; }\n"
  "replaceable { font-style: italic; font-weight: 600; color: »textColor«; }\n"
  "indexterm { display: none; }\n"
  "userinput { color: »userInput«; font-weight: 600; }\n"
  "pre { margin-top: 0px; margin-bottom: 0px; margin-left: 6px; padding: 3px 8px; line-height: 1.5; }\n"
  "pre.programlisting {margin: 6px; padding: 10px; color: »textColor«; display: block; font-size: 95%;"
  " background-color: »userInputBackground«; }\n"
  "</style>";

//----------------------------------------------------------------------------------------------------------------------

std::string convertXRef(long version, std::string const &source) {
  if (source.find("<xref") == std::string::npos)
    return source;

  // We cannot use std::regex here atm, as this crashes on macOS. Maybe later...
  std::string result = source;
  static pcrecpp::RE pattern = "<xref linkend=\"([^\"]+)\" />";
  pattern.GlobalReplace("<a href='http://dev.mysql.com/doc/refman/{0}.{1}/en/\\1.html'>\\1</a>", &result);

  result = base::replaceString(result, "{0}", std::to_string(version / 100));
  result = base::replaceString(result, "{1}", std::to_string(version % 10));

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

std::string convertExternalLinks(long version, std::string const &source) {
  if (source.find("<link") == std::string::npos)
    return source;

  std::string result = source;
  static pcrecpp::RE pattern = "<link linkend=\"([^\"]+)\">([^<]+)<\\/link>";
  pattern.GlobalReplace("<a href='http://dev.mysql.com/doc/refman/{0}.{1}/en/glossary.html#\\1'>\\2</a>", &result);

  result = base::replaceString(result, "{0}", std::to_string(version / 100));
  result = base::replaceString(result, "{1}", std::to_string(version % 10));
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

std::string convertInternalLinks(std::string const &source) {
  if (source.find("role=\"stmt\"") == std::string::npos)
    return source;

  std::string result = source;
  static pcrecpp::RE pattern = "<literal role=\"stmt\">([^<]+)</literal>";
  pattern.GlobalReplace("<a href='local:\\1'>\\1</a>", &result);
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

std::string convertList(long version, Value const &list) {
  std::string result;
  for (auto const &entry: list.GetArray()) {
    auto iterator = entry.FindMember("para");
    if (iterator != entry.MemberEnd()) {
      std::string text = "<p>" + convertInternalLinks(iterator->value.GetString()) +  "</p>";
      result += convertXRef(version, convertExternalLinks(version, text));
    } else {
      auto iterator = entry.FindMember("programlisting");
      if (iterator != entry.MemberEnd()) {
        std::string text = convertInternalLinks(iterator->value.GetString());
        result += "<pre>" + text + "</pre>";
      } else {
        auto iterator = entry.FindMember("itemizedlist"); // Convert to bullet list.
        if (iterator != entry.MemberEnd()) {
          result = "<ul>";
          auto const &itemizedList = iterator->value.GetArray();
          for (auto const &listentry: itemizedList) {
            result += "<li>" + convertList(version, listentry) + "</li>";
          }
          result += "</ul>";
        }
      }
    }
  }

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Creates the HTML formatted help text from the object that's passed in.
 */
std::string DbSqlEditorContextHelp::createHelpTextFromJson(long version, Value const &json) {
  std::string result = "<body>";
  std::string id = json.HasMember("id") ? json["id"].GetString() : "";
  result += "<h3>" + id + " Syntax:</h3>";

  // Syntax (the summary), often in a code block.

  if (json.HasMember("syntax")) {
    auto &syntax = json["syntax"];
    if (syntax.IsObject()) {
      for (auto it = syntax.MemberBegin(); it != syntax.MemberEnd(); ++it) {
        // There are different variants for syntax descriptions. Usually it's encapsulated in a program listing,
        // but e.g. for functions in a list the syntax is a paragraph.
        auto entry = it->value.FindMember("programlisting");
        if (entry != it->value.MemberEnd()) {
          std::string text = convertInternalLinks(entry->value.GetString());
          result += "<pre class='programlisting line-numbers language-sql'>" + text + "</pre><br/>";
        } else {
          auto para = it->value.FindMember("para");
          if (para != it->value.MemberEnd()) {
            result += "<p>" + convertInternalLinks(para->value.GetString()) + "</p>";
          }
        }
      }
    }
  }
  
  // The full description, plain text with code examples, lists and more.
  if (json.HasMember("description")) {
    auto const &description = json["description"].GetArray();
    for (auto const &entry : description) {
      auto iterator = entry.FindMember("para");
      if (iterator != entry.MemberEnd()) {
        std::string text = "<p>" + convertInternalLinks(iterator->value.GetString()) + "</p>";
        result += convertXRef(version, convertExternalLinks(version, text));
      } else {
        auto iterator = entry.FindMember("programlisting");
        if (iterator != entry.MemberEnd()) {
          std::string text = convertInternalLinks(iterator->value.GetString());
          result += "<pre class='programlisting line-numbers language-sql'>" + text + "</pre><br/>";
        } else {
          auto iterator = entry.FindMember("itemizedlist"); // Convert to bullet list.
          if (iterator != entry.MemberEnd()) {
            result += "<ul>";
            auto const &itemizedList = iterator->value.GetArray();
            for (auto const &listentry : itemizedList) {
              result += "<li>" + convertList(version, listentry) + "</li>";
            }
            result += "</ul>";
          }
        }
      }
    }
  }

  std::string page = base::replaceString(base::tolower(id), " ", "-");

  auto iterator = pageMap.find(page);
  if (iterator != pageMap.end())
    page = iterator->second;
  std::string url = base::strfmt("http://dev.mysql.com/doc/refman/%ld.%ld/en/%s.html", version / 100, version % 10, page.c_str());
  result += "<b>See also: </>: <a href='" + url + "'>Online help " + page + "</a><br /><br /></body>";

  return result;
}

//----------------- DbSqlEditorContextHelp -----------------------------------------------------------------------------

DbSqlEditorContextHelp::DbSqlEditorContextHelp() {

  pageMap = {
    { "now", "date-and-time-functions" },
    { "like", "string-comparison-functions" },
    { "auto_increment", "example-auto-increment" },
  };

  loaderThread = std::thread([this]() {
    std::string dataDir = base::makePath(mforms::App::get()->baseDir(), "modules/data/sqlide");
    for (long version : { 800, 507, 506 }) {
      std::string fileName = "help-" + std::to_string(version / 100) + "." + std::to_string(version % 10) + ".json";
      std::string path = base::makePath(dataDir, fileName);
      if (!base::file_exists(path)) {
        logError("Help file not found (%s)\n", path.c_str());
        continue;
      }

      try {
        rapidjson::Value document;
        std::ifstream ifs(path);
        IStreamWrapper isw(ifs);
        Document d;
        d.ParseStream(isw);
        if (d.HasParseError()) {
          logError("Could not read help text file (%s)\nError code: %d\n", fileName.c_str(), d.GetParseError());
          continue;
        }

        std::set<std::string> topics;
        Value topicRoot;
        topicRoot.CopyFrom(d, d.GetAllocator());

        if (topicRoot.HasMember("topics")) {
          auto const &topicList = topicRoot["topics"].GetArray();
          for (auto &topic : topicList) {
            std::string id = base::toupper(topic["id"].GetString());
            topics.insert(id);
            helpContent[version][id] = createHelpTextFromJson(version, topic);
          }
        }
        helpTopics[version] = topics;
      } catch (std::bad_cast &e) {
        logError("Unexpected file format (%s)\nError message: %s\n", fileName.c_str(), e.what());
      }
      
    }
  });
}

//----------------------------------------------------------------------------------------------------------------------

DbSqlEditorContextHelp::~DbSqlEditorContextHelp() {
  waitForLoading();
}

//----------------------------------------------------------------------------------------------------------------------

void DbSqlEditorContextHelp::waitForLoading() {
  if (loaderThread.joinable())
    loaderThread.join();
};

//----------------------------------------------------------------------------------------------------------------------

/**
 * A quick lookup if the help topic exists actually, without retrieving help text.
 */
bool DbSqlEditorContextHelp::topicExists(long serverVersion, const std::string &topic) {
  waitForLoading();

  auto iterator = helpTopics.find(serverVersion / 100);
  if (iterator == helpTopics.end())
    return false;
  return iterator->second.count(topic) > 0;
};

//----------------------------------------------------------------------------------------------------------------------

bool DbSqlEditorContextHelp::helpTextForTopic(HelpContext *context, const std::string &topic, std::string &text) {
  logDebug2("Looking up help topic: %s\n", topic.c_str());

  // If help text is requested so quickly that loading the help content hasn't finished yet, wait here.
  // Usually however, the content is loaded way before offline help is queried the first time.
  waitForLoading();

  if (!topic.empty()) {
    auto iterator = helpContent.find(context->serverVersion() / 100);
    if (iterator == helpContent.end())
      return false;

    // Prepare style sheet depending on the OS theme.
    std::string styleSheet;
#ifndef __linux__
    styleSheet = helpStyleSheetTemplate;

    base::Color color;
    if (mforms::App::get()->isDarkModeActive())
      color = { 0x60 / 255.0, 0x60 / 255.0, 0x60 / 255.0 };
    else
      color = { 0xd0 / 255.0, 0xd0 / 255.0, 0xd0 / 255.0 };
    base::replaceStringInplace(styleSheet, "»literalBackground«", color.to_html());

    color = base::Color::getSystemColor(base::TextColor);
    base::replaceStringInplace(styleSheet, "»textColor«", color.to_html());

    color = base::Color::getSystemColor(base::SecondaryBackgroundColor);
    base::replaceStringInplace(styleSheet, "»mainBackground«", color.to_html());

    if (mforms::App::get()->isDarkModeActive())
      color = base::Color::parse("#404040");
    else
      color = base::Color::parse("#ebebeb");
    base::replaceStringInplace(styleSheet, "»userInputBackground«", color.to_html());

    color = base::Color::parse("#004480");
    base::replaceStringInplace(styleSheet, "»userInput«", color.to_html());
#endif

    text = "<html><head>" + styleSheet + "</head>" + iterator->second[topic] + "</html>";
    return true;
  }
  return false;
}

//----------------------------------------------------------------------------------------------------------------------

// Determines if the given tree is a terminal node and if so, if it is of the given type.
bool isToken(tree::ParseTree *tree, size_t type) {
  auto terminal = dynamic_cast<tree::TerminalNode *>(tree);
  if (terminal != nullptr)
    return terminal->getSymbol()->getType() == type;

  auto token = dynamic_cast<ParserRuleContext *>(tree)->start;
  if (token == nullptr)
    return false;
  return token->getType() == type;
}

//----------------------------------------------------------------------------------------------------------------------

// Determines if the given is of the given type.
bool isToken(Token *token, size_t type) {
  return token->getType() == type;
}

//----------------------------------------------------------------------------------------------------------------------

// Determines if the parent of the given tree is a specific context.
bool isParentContext(tree::ParseTree *tree, size_t type) {
  auto parent = dynamic_cast<ParserRuleContext *>(tree->parent);
  return parent->getRuleIndex() == type;
}

//----------------------------------------------------------------------------------------------------------------------

static std::map<std::string, std::string> functionSynonyms = {
  { "ST_ASWKB", "ST_ASBINARY" },
  { "ASWKB", "ASBINARY" },
  { "ST_ASWKT", "ST_ASTEXT" },
  { "ASWKT", "ASTEXT" },
  { "ST_CROSSES", "CROSSES" },
  { "GEOMETRYFROMTEXT", "GEOMFROMTEXT" },
  { "GEOMETRYFROMWKB", "GEOMFROMWKB" },
  { "ST_GEOMETRYFROMTEXT", "ST_GEOMFROMTEXT" },
  { "ST_GEOMETRYFROMWKB", "ST_GEOMFROMWKB" },
  { "ST_GLENGTH", "GLENGTH" },
};

std::string functionTopicForContext(ParserRuleContext *context) {
  std::string topic;

  Token *nameToken = nullptr;
  size_t rule = context->getRuleIndex();
  switch (rule) {
    case MySQLParser::RuleFunctionCall: {
      auto functionContext = dynamic_cast<MySQLParser::FunctionCallContext *>(context);

      // We only consider global functions here, hence there should not be any qualifier.
      if (functionContext->pureIdentifier() != nullptr)
        nameToken = functionContext->pureIdentifier()->start;
      else if (functionContext->qualifiedIdentifier() != nullptr)
        nameToken = functionContext->qualifiedIdentifier()->start;

      break;
    }

    case MySQLParser::RuleRuntimeFunctionCall: {
      auto functionContext = dynamic_cast<MySQLParser::RuntimeFunctionCallContext *>(context);
      if (functionContext->name != nullptr) {
        switch (functionContext->name->getType()) {
          // Function names that are also keywords.
          case MySQLLexer::IF_SYMBOL:
          case MySQLLexer::REPEAT_SYMBOL:
          case MySQLLexer::REPLACE_SYMBOL:
          case MySQLLexer::TIME_SYMBOL:
          case MySQLLexer::TIMESTAMP_SYMBOL:
          case MySQLLexer::CHAR_SYMBOL:
          case MySQLLexer::DATE_SYMBOL:
          case MySQLLexer::INSERT_SYMBOL:
            return base::toupper(functionContext->name->getText()) + " FUNCTION";

          case MySQLLexer::COLLATION_SYMBOL:
            return "COLLATION";

          default:
            nameToken = functionContext->name;
        }
      }
      break;
    }

    case MySQLParser::RuleSumExpr: {
      auto exprContext = dynamic_cast<MySQLParser::SumExprContext *>(context);
      if (exprContext->COUNT_SYMBOL() != nullptr && exprContext->DISTINCT_SYMBOL() != nullptr)
        return "COUNT DISTINCT";
      nameToken = exprContext->name;

      break;
    }

    case MySQLParser::RuleGeometryFunction: {
      auto functionContext = dynamic_cast<MySQLParser::GeometryFunctionContext *>(context);
      nameToken = functionContext->name;
      break;
    }
  }

  if (nameToken != nullptr)
    topic = base::toupper(nameToken->getText());
  if (functionSynonyms.count(topic) > 0)
    topic = functionSynonyms[topic];

  return topic;
}

//----------------------------------------------------------------------------------------------------------------------

static std::unordered_map<size_t, std::string> supportedOperatorsAndKeywords = {
  { MySQLLexer::EQUAL_OPERATOR, "ASSIGN-EQUAL" },
  { MySQLLexer::ASSIGN_OPERATOR, "ASSIGN-VALUE" },
  { MySQLLexer::LOGICAL_AND_OPERATOR, "AND" },
  { MySQLLexer::LOGICAL_OR_OPERATOR, "||" },
  { MySQLLexer::BIT_AND_SYMBOL, "BIT_AND" },
  { MySQLLexer::BIT_OR_SYMBOL, "BIT_OR" },
  { MySQLLexer::BIT_XOR_SYMBOL, "BIT_XOR" },
  { MySQLLexer::LOGICAL_NOT_OPERATOR, "!" },
  { MySQLLexer::NOT_EQUAL_OPERATOR, "!=" },
  { MySQLLexer::MOD_OPERATOR, "%" },
  { MySQLLexer::BITWISE_AND_OPERATOR, "&" },
  { MySQLLexer::MULT_OPERATOR, "*" },
  { MySQLLexer::PLUS_OPERATOR, "+" },
  { MySQLLexer::JSON_SEPARATOR_SYMBOL, "->" },
  { MySQLLexer::JSON_UNQUOTED_SEPARATOR_SYMBOL, "->>" },
  { MySQLLexer::DIV_OPERATOR, "/" },
  { MySQLLexer::LESS_THAN_OPERATOR, "<" },
  { MySQLLexer::SHIFT_LEFT_OPERATOR, "<<" },
  { MySQLLexer::NULL_SAFE_EQUAL_OPERATOR, "<=>" },
  { MySQLLexer::GREATER_THAN_OPERATOR, ">" },
  { MySQLLexer::GREATER_OR_EQUAL_OPERATOR, ">=" },
  { MySQLLexer::LESS_OR_EQUAL_OPERATOR, "<=" },
  { MySQLLexer::SHIFT_RIGHT_OPERATOR, ">>" },
  { MySQLLexer::BITWISE_XOR_OPERATOR, "^" },
  { MySQLLexer::BITWISE_OR_OPERATOR, "|" },
  { MySQLLexer::BITWISE_NOT_OPERATOR, "~" },

  { MySQLLexer::AUTO_INCREMENT_SYMBOL, "AUTO_INCREMENT" },
  { MySQLLexer::CALL_SYMBOL, "CALL" },
  { MySQLLexer::CAST_SYMBOL, "CAST" },
  { MySQLLexer::DIV_SYMBOL, "DIV" },
  { MySQLLexer::MOD_SYMBOL, "MOD" },
  { MySQLLexer::OR_SYMBOL, "OR" },
  { MySQLLexer::SPATIAL_SYMBOL, "SPATIAL" },
  { MySQLLexer::UNION_SYMBOL, "UNION" },
  { MySQLLexer::XOR_SYMBOL, "XOR" },
};

// Simple token -> topic matches, only used in certain contexts and only if there is no trivial token -> topic
// translation.
static std::unordered_map<size_t, std::string> tokenToTopic = {
  { MySQLLexer::AUTHORS_SYMBOL, "SHOW AUTHORS" },
  { MySQLLexer::BINLOG_SYMBOL, "SHOW BINLOG EVENTS" },
  { MySQLLexer::COLLATION_SYMBOL, "SHOW COLLATION" },
  { MySQLLexer::COLUMNS_SYMBOL, "SHOW COLUMNS" },
  { MySQLLexer::CONTRIBUTORS_SYMBOL, "SHOW CONTRIBUTORS" },
  { MySQLLexer::DATABASES_SYMBOL, "SHOW databases" },
  { MySQLLexer::ENGINE_SYMBOL, "SHOW ENGINE" },
  { MySQLLexer::ENGINES_SYMBOL, "SHOW ENGINES" },
  { MySQLLexer::ERRORS_SYMBOL, "SHOW ERRORS" },
  { MySQLLexer::EVENTS_SYMBOL, "SHOW EVENTS" },
  { MySQLLexer::GRANTS_SYMBOL, "SHOW GRANTS" },
  { MySQLLexer::INDEX_SYMBOL, "SHOW INDEX" },
  { MySQLLexer::INDEXES_SYMBOL, "SHOW INDEX" },
  { MySQLLexer::INSTALL_SYMBOL, "INSTALL PLUGIN" },
  { MySQLLexer::KEYS_SYMBOL, "SHOW INDEX" },
  { MySQLLexer::LOGS_SYMBOL, "SHOW BINARY LOGS" },
  { MySQLLexer::MASTER_SYMBOL, "SHOW MASTER STATUS" },
  { MySQLLexer::OPEN_SYMBOL, "SHOW OPEN TABLES" },
  { MySQLLexer::PLUGIN_SYMBOL, "SHOW PLUGIN" },
  { MySQLLexer::PLUGINS_SYMBOL, "SHOW PLUGINS" },
  { MySQLLexer::PRIVILEGES_SYMBOL, "SHOW PRIVILEGES" },
  { MySQLLexer::PROCESSLIST_SYMBOL, "SHOW PROCESSLIST" },
  { MySQLLexer::PROFILE_SYMBOL, "SHOW PROFILE" },
  { MySQLLexer::PROFILES_SYMBOL, "SHOW PROFILES" },
  { MySQLLexer::RELAYLOG_SYMBOL, "SHOW RELAYLOG EVENTS" },
  { MySQLLexer::STATUS_SYMBOL, "SHOW STATUS" },
  { MySQLLexer::TABLES_SYMBOL, "SHOW TABLES" },
  { MySQLLexer::TRIGGERS_SYMBOL, "SHOW TRIGGERS" },
  { MySQLLexer::VARIABLES_SYMBOL, "SHOW VARIABLES" },
  { MySQLLexer::WARNINGS_SYMBOL, "SHOW WARNINGS" },

  { MySQLLexer::ANALYZE_SYMBOL, "ANALYZE TABLE" },
  { MySQLLexer::CHECKSUM_SYMBOL, "CHECKSUM TABLE" },
  { MySQLLexer::CACHE_SYMBOL, "CACHE INDEX" },
  { MySQLLexer::CHECK_SYMBOL, "CHECK TABLE" },
  { MySQLLexer::FLUSH_SYMBOL, "FLUSH" },
  { MySQLLexer::KILL_SYMBOL, "KILL" },
  { MySQLLexer::LOAD_SYMBOL, "LOAD INDEX" },
  { MySQLLexer::OPTIMIZE_SYMBOL, "OPTIMIZE TABLE" },
  { MySQLLexer::REPAIR_SYMBOL, "REPAIR TABLE" },
  { MySQLLexer::SHUTDOWN_SYMBOL, "SHUTDOWN" },
  { MySQLLexer::UNINSTALL_SYMBOL, "UNINSTALL PLUGIN" },
};

static std::unordered_map<size_t, std::string> contextToTopic = {
  { MySQLParser::RuleCallStatement, "CALL" },
  { MySQLParser::RuleCreateDatabase, "CREATE DATABASE" },
  { MySQLParser::RuleCreateEvent, "CREATE EVENT" },
  { MySQLParser::RuleCreateFunction, "CREATE FUNCTION" },
  { MySQLParser::RuleCreateUdf, "CREATE FUNCTION UDF" },
  { MySQLParser::RuleCreateIndex, "CREATE INDEX" },
  { MySQLParser::RuleCreateProcedure, "CREATE PROCEDURE" },
  { MySQLParser::RuleCreateServer, "CREATE SERVER" },
  { MySQLParser::RuleCreateTable, "CREATE TABLE" },
  { MySQLParser::RuleCreateTablespace, "CREATE TABLESPACE" },
  { MySQLParser::RuleCreateTrigger, "CREATE TRIGGER" },
  { MySQLParser::RuleCreateUser, "CREATE USER" },
  { MySQLParser::RuleCreateView, "CREATE VIEW" },
  { MySQLParser::RuleDeleteStatement, "DELETE" },
  { MySQLParser::RuleDoStatement, "DO" },
  { MySQLParser::RuleDropUser, "DROP USER" },
  { MySQLParser::RuleExecuteStatement, "EXECUTE STATEMENT" },
  { MySQLParser::RuleDescribeStatement, "EXPLAIN" },
  { MySQLParser::RuleExplainStatement, "EXPLAIN" },
  { MySQLParser::RuleGrant, "GRANT" },
  { MySQLParser::RuleHandlerStatement, "HANDLER" },
  { MySQLParser::RuleHandlerDeclaration, "DECLARE HANDLER" },
  { MySQLParser::RuleHelpCommand, "HELP COMMAND" },
  { MySQLParser::RuleIfStatement, "IF STATEMENT" },
  { MySQLParser::RuleIterateStatement, "ITERATE" },
  { MySQLParser::RuleJoinedTable, "JOIN" },
  { MySQLParser::RuleLabel, "LABELS" },
  { MySQLParser::RuleLeaveStatement, "LEAVE" },
  { MySQLParser::RuleLockStatement, "LOCK" },
  { MySQLParser::RuleLoopBlock, "LOOP" },
  { MySQLParser::RuleCursorOpen, "OPEN" },
  { MySQLParser::RuleQuerySpecification, "SELECT" },
  { MySQLParser::RuleCursorClose, "CLOSE" },
  { MySQLParser::RuleCursorFetch, "FETCH" },
  { MySQLParser::RuleProcedureAnalyseClause, "PROCEDURE ANALYSE" },
  { MySQLParser::RuleRenameTableStatement, "RENAME TABLE" },
  { MySQLParser::RuleRenameUser, "RENAME USER" },
  { MySQLParser::RuleRepeatUntilBlock, "REPEAT LOOP" },
  { MySQLParser::RuleReplaceStatement, "REPLACE" },
  { MySQLParser::RuleResignalStatement, "RESIGNAL" },
  { MySQLParser::RuleReturnStatement, "RETURN" },
  { MySQLParser::RuleRevoke, "REVOKE" },
  { MySQLParser::RuleSavepointStatement, "SAVEPOINT" },
  { MySQLParser::RuleSelectStatement, "SELECT" },
  { MySQLParser::RuleTransactionStatement, "START TRANSACTION" },
  { MySQLParser::RuleTruncateTableStatement, "TRUNCATE TABLE" },
  { MySQLParser::RuleUpdateStatement, "UPDATE" },
  { MySQLParser::RuleUseCommand, "USE" },
  { MySQLParser::RuleWhileDoBlock, "WHILE" },
  { MySQLParser::RuleXaStatement, "XA" },
  { MySQLParser::RuleVariableDeclaration, "DECLARE VARIABLE" },
  { MySQLParser::RuleConditionDeclaration, "DECLARE CONDITION" },
  { MySQLParser::RuleHandlerDeclaration, "DECLARE HANDLER" },
  { MySQLParser::RuleCursorDeclaration, "DECLARE CURSOR" },
  { MySQLParser::RuleGetDiagnostics, "GET DIAGNOSTICS" },
  { MySQLParser::RuleSignalStatement, "SIGNAL" },
  { MySQLParser::RuleAlterUser, "ALTER USER" },
  { MySQLParser::RuleCaseStatement, "CASE STATEMENT" },
  { MySQLParser::RuleChangeMaster, "CHANGE MASTER TO" },

  { MySQLParser::RuleDropDatabase, "DROP DATABASE" },
  { MySQLParser::RuleDropEvent, "DROP EVENT" },
  { MySQLParser::RuleDropFunction, "DROP FUNCTION" },
  { MySQLParser::RuleDropProcedure, "DROP PROCEDURE" },
  { MySQLParser::RuleDropIndex, "DROP INDEX" },
  { MySQLParser::RuleDropLogfileGroup, "DROP LOGFILEGROUP" },
  { MySQLParser::RuleDropServer, "DROP SERVER" },
  { MySQLParser::RuleDropTable, "DROP TABLE" },
  { MySQLParser::RuleDropTableSpace, "DROP TABLESPACE" },
  { MySQLParser::RuleDropTrigger, "DROP TRIGGER" },
  { MySQLParser::RuleDropView, "DROP VIEW" },

};

// Words which are part of a multi word topic or can produce wrong topics if used alone, and hence need further
// examination.
static std::unordered_set<std::string> specialWords = {
  "CHAR", "COUNT",    "DATE",    "DOUBLE",    "REPLACE", "TIME",   "TIMESTAMP", "YEAR", "DATABASE",
  "USER", "INSERT",   "PREPARE", "HANDLER",   "FLUSH",   "IS",     "IN",        "LIKE", "REGEXP",
  "SET",  "PASSWORD", "SHOW",    "COLLATION", "OPEN",    "UPDATE", "DELETE"
};

//----------------------------------------------------------------------------------------------------------------------

/**
 * Determines a help topic from the given query at the given position (given as column/row pair).
 */
std::string DbSqlEditorContextHelp::helpTopicFromPosition(HelpContext *helpContext, const std::string &query,
                                                          size_t caret) {
  logDebug2("Finding help topic\n");

  // We are not interested in validity here. We simply parse in default mode (LL) and examine the returned parse tree.
  // This usually will give us a good result, except in cases where the query has an error before the caret such that
  // we cannot predict the path through the rules.
  ParserRuleContext *parseTree = helpContext->_d->parse(query);
  //std::cout << "Parse tree: " << parseTree->toStringTree(&helpContext->_d->parser) << std::endl;

  tree::ParseTree *tree = MySQLRecognizerCommon::contextFromPosition(parseTree, caret);

  if (tree == nullptr)
    return "";

  if (antlrcpp::is<tree::TerminalNode *>(tree)) {
    tree::TerminalNode *node = dynamic_cast<tree::TerminalNode *>(tree);
    size_t token = node->getSymbol()->getType();
    if (token == MySQLLexer::SEMICOLON_SYMBOL) {
      tree = MySQLRecognizerCommon::getPrevious(tree);
      node = dynamic_cast<tree::TerminalNode *>(tree);
      token = node->getSymbol()->getType();
    }

    // First check if we can get a topic for this single token, either from our topic table or by lookup.
    // This is a double-edged sword. It will help in incomplete statements where we do not get a good parse tree
    // but might also show help topics for unrelated stuff (e.g. returns "SAVEPOINT" for "select a := savepoint(c);").
    if (supportedOperatorsAndKeywords.count(token) > 0)
      return supportedOperatorsAndKeywords[token];

    switch (token) {
      case MySQLLexer::MINUS_OPERATOR:
        if (isParentContext(tree, MySQLParser::RuleSimpleExpr))
          return "- UNARY";
        return "- BINARY";

      case MySQLLexer::BINARY_SYMBOL:
        if (isParentContext(tree, MySQLParser::RuleDataType))
          return "BINARY";
        if (isParentContext(tree, MySQLParser::RuleSimpleExpr) || isParentContext(tree, MySQLParser::RuleCastType))
          return "BINARY OPERATOR";

        tree = tree->parent;
        break;

      case MySQLLexer::CHANGE_SYMBOL:
        if (isParentContext(tree, MySQLParser::RuleChangeMaster))
          return "CHANGE MASTER TO";
        if (isParentContext(tree, MySQLParser::RuleChangeReplication))
          return "CHANGE REPLICATION FILTER";

        tree = tree->parent;
        break;

      // Other keywords connected to topics.
      case MySQLLexer::BEGIN_SYMBOL:
      case MySQLLexer::END_SYMBOL:
        return "BEGIN END";

      case MySQLLexer::TRUE_SYMBOL:
      case MySQLLexer::FALSE_SYMBOL:
        return "TRUE FALSE";

      case MySQLLexer::BINLOG_SYMBOL:
        if (isParentContext(tree, MySQLParser::RuleOtherAdministrativeStatement))
          return "BINLOG";
        if (isParentContext(tree, MySQLParser::RuleShowStatement))
          return "SHOW BINLOG EVENTS";

        tree = tree->parent;
        break;

      default:
        std::string s = base::toupper(node->getText());
        if (specialWords.count(s) == 0 && topicExists(helpContext->serverVersion(), s))
          return s;

        // No specific help topic for the given terminal. Jump to the token's parent and start the
        // context search then.
        tree = tree->parent;
        break;
    }
  }

  // See if we have a help topic for the given tree. If not walk up the parent chain until we find something.
  while (true) {
    if (tree == nullptr)
      return "";

    // We deliberately don't check if the given tree is actually a parse rule context - there is no other possibility.
    ParserRuleContext *context = dynamic_cast<ParserRuleContext *>(tree);
    size_t ruleIndex = context->getRuleIndex();
    if (contextToTopic.count(ruleIndex) > 0)
      return contextToTopic[ruleIndex];

    // Topics from function names
    std::string functionTopic = functionTopicForContext(context);
    if (!functionTopic.empty() && topicExists(helpContext->serverVersion(), functionTopic))
      return functionTopic;

    switch (ruleIndex) {
      case MySQLParser::RulePredicateOperations: {
        if (!context->children.empty()) {
          // Some keyword topics have variants with a leading NOT.
          auto parent = dynamic_cast<MySQLParser::PredicateContext *>(context->parent);
          bool isNot = parent->notRule() != nullptr;

          // IN, BETWEEN (with special help topic name), LIKE, REGEXP
          auto predicateContext = dynamic_cast<MySQLParser::PredicateOperationsContext *>(context);
          if (isToken(predicateContext->children[0], MySQLLexer::BETWEEN_SYMBOL)) {
            if (isNot)
              return "NOT BETWEEN";
            return "BETWEEN AND";
          }
          std::string topic = isNot ? "NOT " : "";
          return topic + base::toupper(predicateContext->children[0]->getText());
        }
        break;
      }

      case MySQLParser::RuleOtherAdministrativeStatement: {
        // See if we only have a single flush command.
        auto adminContext = dynamic_cast<MySQLParser::OtherAdministrativeStatementContext *>(context);
        if (adminContext->type->getType() == MySQLLexer::FLUSH_SYMBOL && adminContext->flushOption().size() == 1 &&
            adminContext->flushOption(0)->option->getType() == MySQLLexer::QUERY_SYMBOL)
          return "FLUSH QUERY CACHE";

        if (adminContext->type != nullptr)
          return tokenToTopic[adminContext->type->getType()];
        break;
      }

      case MySQLParser::RuleInsertStatement: {
        auto insertContext = dynamic_cast<MySQLParser::InsertStatementContext *>(context);
        if (insertContext->insertQueryExpression() != nullptr)
          return "INSERT SELECT";
        if (insertContext->insertLockOption() != nullptr &&
            insertContext->insertLockOption()->DELAYED_SYMBOL() != nullptr)
          return "INSERT DELAYED";
        return "INSERT";
      }

      case MySQLParser::RuleInstallUninstallStatment: {
        auto pluginContext = dynamic_cast<MySQLParser::InstallUninstallStatmentContext *>(context);
        if (pluginContext->action != nullptr)
          return tokenToTopic[pluginContext->action->getType()];
        break;
      }

      case MySQLParser::RuleExpr: {
        auto exprContext = dynamic_cast<MySQLParser::ExprContext *>(context);
        if (exprContext->children.size() > 2 && isToken(exprContext->children[1], MySQLLexer::IS_SYMBOL)) {
          if (isToken(exprContext->children[2], MySQLLexer::NOT_SYMBOL) ||
              isToken(exprContext->children[2], MySQLLexer::NOT2_SYMBOL))
            return "IS NOT";
          return "IS";
        }
        break;
      }

      case MySQLParser::RuleBoolPri: {
        if (antlrcpp::is<MySQLParser::PrimaryExprIsNullContext *>(context)) {
          auto primaryExprIsNullContext = dynamic_cast<MySQLParser::PrimaryExprIsNullContext *>(context);
          if (primaryExprIsNullContext->notRule() == nullptr)
            return "IS NULL";
          return "IS NOT NULL";
        }
        break;
      }

      case MySQLParser::RuleSetStatement: {
        auto setContext = dynamic_cast<MySQLParser::SetStatementContext *>(context);
        auto setValueListContext = setContext->startOptionValueList();
        if (setValueListContext->TRANSACTION_SYMBOL() != nullptr)
          return "ISOLATION";

        if (setValueListContext->PASSWORD_SYMBOL().size() > 0)
          return "SET PASSWORD";

        // TODO: handle the assignment at the caret instead of looking for specific names in the first assignment.
        ParserRuleContext *variableName = nullptr;
        if (setValueListContext->startOptionValueListFollowingOptionType() != nullptr) {
          auto subContext = setValueListContext->startOptionValueListFollowingOptionType();
          variableName = subContext->optionValueFollowingOptionType()->internalVariableName();
        } else if (setValueListContext->optionValueNoOptionType() != nullptr)
          variableName = setValueListContext->optionValueNoOptionType()->internalVariableName();
        if (variableName != nullptr) {
          std::string option = base::toupper(variableName->getText());
          if (option == "SQL_SLAVE_SKIP_COUNTER")
            return "SET GLOBAL SQL_SLAVE_SKIP_COUNTER";
          if (option == "SQL_LOG_BIN")
            return "SET SQL_LOG_BIN";
        }
        return "SET";
      }

      case MySQLParser::RuleLoadStatement: {
        auto loadStatementContext = dynamic_cast<MySQLParser::LoadStatementContext *>(context);
        if (loadStatementContext->dataOrXml()->DATA_SYMBOL() != nullptr)
          return "LOAD DATA";
        return "LOAD XML";
      }

      case MySQLParser::RulePredicate:
        if (context->children.size() > 2) {
          if (isToken(context->children[1], MySQLLexer::NOT_SYMBOL) ||
              isToken(context->children[1], MySQLLexer::NOT2_SYMBOL)) {
            // For NOT BETWEEN, NOT LIKE, NOT IN, NOT REGEXP.
            auto predicateContext = dynamic_cast<MySQLParser::PredicateOperationsContext *>(context->children[2]);
            return "NOT " + base::toupper(predicateContext->children[0]->getText());
          }
          if (isToken(context->children[1], MySQLLexer::SOUNDS_SYMBOL))
            return "SOUNDS LIKE";
        }
        break;

      case MySQLParser::RuleTableAdministrationStatement: {
        auto adminStatementContext = dynamic_cast<MySQLParser::TableAdministrationStatementContext *>(context);
        if (adminStatementContext->type != nullptr)
          return tokenToTopic[adminStatementContext->type->getType()];
        break;
      }

      case MySQLParser::RulePreparedStatement: {
        auto preparedContext = dynamic_cast<MySQLParser::PreparedStatementContext *>(context);
        size_t type = 0;
        if (preparedContext->type != nullptr)
          type = preparedContext->type->getType();
        if (type == MySQLLexer::PREPARE_SYMBOL)
          return "PREPARE";
        if (type == MySQLLexer::DEALLOCATE_SYMBOL || type == MySQLLexer::DROP_SYMBOL)
          return "DEALLOCATE PREPARE";
        break;
      }

      case MySQLParser::RuleReplicationStatement: {
        auto replicationContext = dynamic_cast<MySQLParser::ReplicationStatementContext *>(context);
        if (replicationContext->PURGE_SYMBOL() != nullptr)
          return "PURGE BINARY LOGS";
        if (replicationContext->RESET_SYMBOL() != nullptr &&
            (replicationContext->resetOption().empty() || replicationContext->resetOption()[0]->option == nullptr))
          return "RESET";
        break;
      }

      case MySQLParser::RuleResetOption: {
        auto optionContext = dynamic_cast<MySQLParser::ResetOptionContext *>(context);
        if (isToken(optionContext->option, MySQLLexer::MASTER_SYMBOL))
          return "RESET MASTER";
        if (isToken(optionContext->option, MySQLLexer::SLAVE_SYMBOL))
          return "RESET SLAVE";
        return "RESET";
      }

      case MySQLParser::RuleShowStatement: {
        auto showContext = dynamic_cast<MySQLParser::ShowStatementContext *>(context);
        if (showContext->value == nullptr) {
          if (showContext->charset() != nullptr)
            return "SHOW CHARACTER SET";
          return "SHOW";
        }

        switch (showContext->value->getType()) {
          case MySQLLexer::TABLE_SYMBOL:
            return "SHOW TABLE STATUS";
          case MySQLLexer::SLAVE_SYMBOL:
            if (showContext->HOSTS_SYMBOL() != nullptr)
              return "SHOW SLAVE HOSTS";
            if (showContext->STATUS_SYMBOL() != nullptr)
              return "SHOW SLAVE STATUS";
            break;
          case MySQLLexer::CREATE_SYMBOL: {
            if (showContext->object != nullptr)
              return "SHOW CREATE " + base::toupper(showContext->object->getText());
            break;
          }
          case MySQLLexer::PROCEDURE_SYMBOL:
            if (showContext->STATUS_SYMBOL() != nullptr)
              return "SHOW PROCEDURE STATUS";
            if (showContext->CODE_SYMBOL() != nullptr)
              return "SHOW PROCEDURE CODE";
            break;
          case MySQLLexer::FUNCTION_SYMBOL:
            if (showContext->STATUS_SYMBOL() != nullptr)
              return "SHOW FUNCTION STATUS";
            if (showContext->CODE_SYMBOL() != nullptr)
              return "SHOW FUNCTION CODE";
            break;

          default:
            return tokenToTopic[showContext->value->getType()];
        }

        break;
      }

      case MySQLParser::RuleTableConstraintDef: {
        auto definitionContext = dynamic_cast<MySQLParser::TableConstraintDefContext *>(context);
        if (definitionContext->type != nullptr && definitionContext->type->getType() == MySQLLexer::FOREIGN_SYMBOL)
          return "CONSTRAINT";
        if (definitionContext->checkConstraint() != nullptr)
          return "CONSTRAINT"; // TODO: There's no own topic for check constraints so far. Update when that is available.
        break;
      }

      case MySQLParser::RuleHelpCommand:
        return "HELP COMMAND";

      case MySQLParser::RuleSimpleExpr:
        if (!context->children.empty() && antlrcpp::is<tree::TerminalNode *>(context->children[0])) {
          size_t type = dynamic_cast<tree::TerminalNode *>(context->children[0])->getSymbol()->getType();
          switch (type) {
            case MySQLLexer::MATCH_SYMBOL:
              return "MATCH AGAINST";
            case MySQLLexer::CONVERT_SYMBOL:
              return "CONVERT";
            case MySQLLexer::DEFAULT_SYMBOL:
              return "DEFAULT";
            case MySQLLexer::CASE_SYMBOL:
              return "CASE OPERATOR";
          }
        }
        break;

      case MySQLParser::RuleEngineRef: {
        std::string engine = base::tolower(context->getText());
        if (engine == "merge" || engine == "mrg_myisam")
          return "MERGE";
        break;
      }

      case MySQLParser::RuleSlave:
        if (!context->children.empty())
          return base::toupper(context->children[0]->getText()) + " SLAVE";
        break;

      case MySQLParser::RuleDataType: {
        auto typeContext = dynamic_cast<MySQLParser::DataTypeContext *>(context);
        if (typeContext->nchar() != nullptr)
          return "CHAR";

        std::string topic;
        switch (typeContext->type->getType()) {
          case MySQLLexer::DOUBLE_SYMBOL:
            if (typeContext->PRECISION_SYMBOL() != nullptr)
              return "DOUBLE PRECISION";
            return "DOUBLE";

          case MySQLLexer::SET_SYMBOL:
            return "SET DATA TYPE";

          case MySQLLexer::YEAR_SYMBOL:
            return "YEAR DATA TYPE";

          case MySQLLexer::BOOL_SYMBOL:
            return "BOOLEAN";

          case MySQLLexer::CHAR_SYMBOL:
          case MySQLLexer::NCHAR_SYMBOL:
          case MySQLLexer::NATIONAL_SYMBOL:
          case MySQLLexer::VARCHAR_SYMBOL:
          case MySQLLexer::NVARCHAR_SYMBOL:
          case MySQLLexer::VARYING_SYMBOL:
            if (typeContext->VARYING_SYMBOL() != nullptr || typeContext->VARCHAR_SYMBOL() != nullptr)
              return "VARCHAR";
            if (typeContext->charsetWithOptBinary() != nullptr &&
                typeContext->charsetWithOptBinary()->BYTE_SYMBOL() != nullptr)
              return "CHAR BYTE";
            return "CHAR";

          default:
            topic = base::toupper(typeContext->type->getText());
            break;
        }

        if (topicExists(helpContext->serverVersion(), topic))
          return topic;

        break; // Not all data types have an own topic.
      }

      case MySQLParser::RuleFromClause: {
        auto keylistContext = dynamic_cast<MySQLParser::FromClauseContext *>(context);
        if (keylistContext->DUAL_SYMBOL() != nullptr)
          return "DUAL";
        break;
      }

      case MySQLParser::RuleTransactionCharacteristics: {
        auto characteristicsContext = dynamic_cast<MySQLParser::TransactionCharacteristicsContext *>(context);
        if (characteristicsContext->isolationLevel() != nullptr)
          return "ISOLATION";
        break;
      }

      case MySQLParser::RuleSubstringFunction: {
        // A case where we might have a synonym, so we need to check the text actually.
        auto substringContext = dynamic_cast<MySQLParser::SubstringFunctionContext *>(context);
        return base::toupper(substringContext->SUBSTRING_SYMBOL()->getText());

        break;
      }

      case MySQLParser::RuleAlterStatement: {
        auto alterContext = dynamic_cast<MySQLParser::AlterStatementContext *>(context);
        if (alterContext->alterTable() != nullptr)
          return "ALTER TABLE";
        if (alterContext->alterDatabase() != nullptr)
          return "ALTER DATABASE";
        if (alterContext->PROCEDURE_SYMBOL() != nullptr)
          return "ALTER PROCEDURE";
        if (alterContext->FUNCTION_SYMBOL() != nullptr)
          return "ALTER FUNCTION";
        if (alterContext->alterView() != nullptr)
          return "ALTER VIEW";
        if (alterContext->alterEvent() != nullptr)
          return "ALTER EVENT";
        if (alterContext->alterTablespace() != nullptr || alterContext->alterUndoTablespace() != nullptr)
          return "ALTER TABLESPACE";
        if (alterContext->alterLogfileGroup() != nullptr)
          return "ALTER LOGFILE GROUP";
        if (alterContext->alterServer() != nullptr)
          return "ALTER SERVER";
        if (alterContext->INSTANCE_SYMBOL() != nullptr)
          return "ALTER INSTANCE";

        break;
      }

      case MySQLParser::RuleCreateStatement: {
        auto createContext = dynamic_cast<MySQLParser::CreateStatementContext *>(context);
        if (createContext->createDatabase() != nullptr)
          return "CREATE DATABASE";
        if (createContext->createTable() != nullptr)
          return "CREATE TABLE";
        if (createContext->createFunction() != nullptr)
          return "CREATE FUNCTION";
        if (createContext->createProcedure() != nullptr)
          return "CREATE PROCEDURE";
        if (createContext->createUdf() != nullptr)
          return "CREATE FUNCTION UDF";
        if (createContext->createLogfileGroup() != nullptr)
          return "CREATE LOGFILE GROUP";
        if (createContext->createView() != nullptr)
          return "CREATE VIEW";
        if (createContext->createTrigger() != nullptr)
          return "CREATE TRIGGER";
        if (createContext->createIndex() != nullptr)
          return "CREATE INDEX";
        if (createContext->createServer() != nullptr)
          return "CREATE SERVER";
        if (createContext->createTablespace() != nullptr)
          return "CREATE TABLESPACE";
        if (createContext->createEvent() != nullptr)
          return "CREATE EVENT";
        if (createContext->createRole() != nullptr)
          return "CREATE ROLE";
        if (createContext->createSpatialReference() != nullptr)
          return "CREATE SPATIALREFERENCE SYSTEM";
        if (createContext->createUndoTablespace() != nullptr)
          return "CREATE TABLESPACE";

        break;
      }

      case MySQLParser::RuleDropStatement: {
        auto dropContext = dynamic_cast<MySQLParser::DropStatementContext *>(context);
        if (dropContext->dropDatabase() != nullptr)
          return "DROP DATABASE";
        if (dropContext->dropEvent() != nullptr)
          return "DROP EVENT";
        if (dropContext->dropFunction() != nullptr) // We cannot make a distinction between FUNCTION and FUNCTION UDF.
          return "DROP FUNCTION";
        if (dropContext->dropProcedure() != nullptr)
          return "DROP PROCEDURE";
        if (dropContext->dropIndex() != nullptr)
          return "DROP INDEX";
        if (dropContext->dropLogfileGroup() != nullptr)
          return "DROP LOGFILE GROUP";
        if (dropContext->dropServer() != nullptr)
          return "DROP SERVER";
        if (dropContext->dropTable() != nullptr)
          return "DROP TABLE";
        if (dropContext->dropTableSpace() != nullptr)
          return "DROP TABLESPACE";
        if (dropContext->dropTrigger() != nullptr)
          return "DROP TRIGGER";
        if (dropContext->dropView() != nullptr)
          return "DROP VIEW";
        if (dropContext->dropRole() != nullptr)
          return "DROP ROLE";
        if (dropContext->dropSpatialReference() != nullptr)
          return "DROP SPATIALREFERENCE SYSTEM";
        if (dropContext->dropUndoTablespace() != nullptr)
          return "DROP TABLESPACE";

        break;
      }
    }

    tree = tree->parent;
  }

  return "";
}

//----------------------------------------------------------------------------------------------------------------------
