/*
 * Copyright (c) 2007, 2022, Oracle and/or its affiliates.
 *
 * 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 designed to work 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 either included with
 * the program or referenced in the documentation.
 * 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 "wb_sql_editor_form.h"

#include "grtdb/db_helpers.h"
#include "grtsqlparser/sql_facade.h"
#include "grtdb/editor_dbobject.h"
#include "grtdb/db_object_helpers.h"

#include "grtui/confirm_save_dialog.h"

#include "sqlide/recordset_be.h"
#include "sqlide/recordset_cdbc_storage.h"
#include "sqlide/wb_sql_editor_snippets.h"
#include "sqlide/wb_sql_editor_panel.h"
#include "sqlide/wb_sql_editor_result_panel.h"
#include "sqlide/wb_sql_editor_tree_controller.h"
#include "sqlide/sql_script_run_wizard.h"

#include "sqlide/column_width_cache.h"

#include "objimpl/db.query/db_query_Resultset.h"
#include "objimpl/wrapper/mforms_ObjectReference_impl.h"

#include "base/string_utilities.h"
#include "base/notifications.h"
#include "base/sqlstring.h"
#include "base/file_functions.h"
#include "base/file_utilities.h"
#include "base/log.h"
#include "base/boost_smart_ptr_helpers.h"
#include "base/util_functions.h"
#include "base/scope_exit_trigger.h"
#include "base/threading.h"

#include "workbench/wb_command_ui.h"
#include "workbench/wb_context_names.h"
#include "workbench/SSHSessionWrapper.h"
#include "workbench/wb_context_ui.h"
#include "workbench/wb_context.h"

#include <mysql_connection.h>

#include <boost/signals2/connection.hpp>

#include "query_side_palette.h"

#include "mforms/menubar.h"
#include "mforms/hypertext.h" // needed for d-tor
#include "mforms/tabview.h"   // needed for d-tor
#include "mforms/splitter.h"  // needed for d-tor
#include "mforms/toolbar.h"
#include "mforms/code_editor.h"

#include "grtsqlparser/mysql_parser_services.h"

#include "wb_tunnel.h"

#include <math.h>
#include <mutex>
#include <thread>

using namespace bec;
using namespace grt;
using namespace wb;
using namespace base;
using namespace parsers;

using boost::signals2::scoped_connection;

using namespace std::string_literals;

DEFAULT_LOG_DOMAIN("SQL Editor Form")

static const char *SQL_EXCEPTION_MSG_FORMAT = _("Error Code: %i\n%s");
static const char *EXCEPTION_MSG_FORMAT = _("Error: %s");

#define CATCH_SQL_EXCEPTION_AND_DISPATCH(statement, log_message_index, duration)                        \
  catch (sql::SQLException & e) {                                                                       \
    set_log_message(log_message_index, DbSqlEditorLog::ErrorMsg,                                        \
                    strfmt(SQL_EXCEPTION_MSG_FORMAT, e.getErrorCode(), e.what()), statement, duration); \
  }

#define CATCH_EXCEPTION_AND_DISPATCH(statement)                                                       \
  catch (std::exception & e) {                                                                        \
    add_log_message(DbSqlEditorLog::ErrorMsg, strfmt(EXCEPTION_MSG_FORMAT, e.what()), statement, ""); \
  }

#define CATCH_ANY_EXCEPTION_AND_DISPATCH(statement)                                                                    \
  catch (sql::SQLException & e) {                                                                                      \
    add_log_message(DbSqlEditorLog::ErrorMsg, strfmt(SQL_EXCEPTION_MSG_FORMAT, e.getErrorCode(), e.what()), statement, \
                    "");                                                                                               \
  }                                                                                                                    \
  CATCH_EXCEPTION_AND_DISPATCH(statement)

#define CATCH_ANY_EXCEPTION_AND_DISPATCH_TO_DEFAULT_LOG(statement)                                        \
  catch (sql::SQLException & e) {                                                                         \
    grt::GRT::get()->send_error(strfmt(SQL_EXCEPTION_MSG_FORMAT, e.getErrorCode(), e.what()), statement); \
  }                                                                                                       \
  catch (std::exception & e) {                                                                            \
    grt::GRT::get()->send_error(strfmt(EXCEPTION_MSG_FORMAT, e.what()), statement);                       \
  }

db_mgmt_ServerInstanceRef getServerInstance(const db_mgmt_ConnectionRef &connection) {
  grt::ValueRef ret = grt::GRT::get()->get("/wb/rdbmsMgmt/storedInstances");
  if (grt::ListRef<db_mgmt_ServerInstance>::can_wrap(ret))
  {
    auto list = grt::ListRef<db_mgmt_ServerInstance>::cast_from(ret);
    for (const auto &item: list) {
      if (item->connection() == connection)
        return item;
    }
  }

  return db_mgmt_ServerInstanceRef();
}

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

class Timer {
public:
  Timer(bool run_immediately) : _is_running(false), _start_timestamp(0), _duration(0) {
    if (run_immediately)
      run();
  }
  void reset() {
    _is_running = false;
    _start_timestamp = 0;
    _duration = 0;
  }
  void run() {
    if (_is_running)
      return;
    _is_running = true;
    _start_timestamp = timestamp();
  }
  void stop() {
    if (!_is_running)
      return;
    _is_running = false;
    _duration += timestamp() - _start_timestamp;
  }
  double duration() {
    return _is_running ? (_duration + timestamp() - _start_timestamp) : _duration;
  }
  std::string duration_formatted() {
    double d = duration(), dd;
    dd = d;
    int zeroes = 1;

    while (dd < 1.0 && dd > 0.0) {
      zeroes++;
      dd *= 10;
    }

    return strfmt(strfmt(_("%%.%if sec"), std::max(3, zeroes)).c_str(), d);
  }

private:
  bool _is_running;
  double _start_timestamp;
  double _duration;
};

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

struct SqlEditorForm::PrivateMutex {
  std::mutex _symbolsMutex;
};

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

SqlEditorForm::Ref SqlEditorForm::create(wb::WBContextSQLIDE *wbsql, const db_mgmt_ConnectionRef &conn) {
  SqlEditorForm::Ref instance(new SqlEditorForm(wbsql));

  if (conn.is_valid())
    instance->set_connection(conn);

  return instance;
}

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

void SqlEditorForm::set_tab_dock(mforms::DockingPoint *dp) {
  _tabdock = dp;
  grtobj()->dockingPoint(mforms_to_grt(dp));
  scoped_connect(_tabdock->signal_view_switched(), std::bind(&SqlEditorForm::sql_editor_panel_switched, this));
  scoped_connect(_tabdock->signal_view_undocked(),
                 std::bind(&SqlEditorForm::sql_editor_panel_closed, this, std::placeholders::_1));
}

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

void SqlEditorForm::report_connection_failure(const std::string &error, const db_mgmt_ConnectionRef &target) {
  std::string message;
  logError("SQL editor could not be connected: %s\n", error.c_str());
  mforms::App::get()->set_status_text(_("Could not connect to target database."));

  if (error.find("exceeded the 'max_user_connections' resource") != std::string::npos) {
    mforms::Utilities::show_error(_("Could not Connect to Database Server"),
                                  base::strfmt("%s\n\nMySQL Workbench requires at least 2 connections to the server, "
                                               "one for management purposes and another for user queries.",
                                               error.c_str()),
                                  "OK");
    return;
  }

  message =
    "Your connection attempt failed for user '%user%' to the MySQL server at %server%:%port%:\n  %error%\n"
    "\n"
    "Please:\n"
    "1 Check that MySQL is running on address %server%\n"
    "2 Check that MySQL is reachable on port %port% (note: 3306 is the default, but this can be changed)\n"
    "3 Check the user %user% has rights to connect to %server% from your address (MySQL rights define what clients can "
    "connect to the server and from which machines) \n"
    "4 Make sure you are both providing a password if needed and using the correct password for %server% connecting "
    "from the host address you're connecting from";

  message = base::replaceString(message, "%user%", target->parameterValues().get_string("userName"));
  message = base::replaceString(message, "%port%", target->parameterValues().get("port").toString());
  message = base::replaceString(message, "%server%", target->parameterValues().get_string("hostName", "localhost"));
  message = base::replaceString(message, "%error%", error);

  logError("%s", (message + '\n').c_str());
  mforms::Utilities::show_error(_("Cannot Connect to Database Server"), message, _("Close"));
}

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

void SqlEditorForm::report_connection_failure(const grt::server_denied &info, const db_mgmt_ConnectionRef &target) {
  std::string message;

  logError("Server is alive, but has login restrictions: %d, %s\n", info.errNo, info.what());

  mforms::App::get()->set_status_text(_("Connection restricted"));

  message = "Your connection attempt failed for user '";
  message += target->parameterValues().get_string("userName");
  message += "' from your host to server at "; //%server%:%port%\n";
  message += target->parameterValues().get_string("hostName", "localhost");
  message += ":";
  message += target->parameterValues().get("port").toString() + "\n";
  if (info.errNo == 3159)
    message += "Only connections with enabled SSL support are accepted.\n";
  else if (info.errNo == 3032)
    message += "The server is in super-user mode and does not accept any other connection.\n";

  message += "\nThe server response was:\n";
  message += info.what();

  mforms::Utilities::show_error(_("Cannot Connect to Database Server"), message, _("Close"));
}

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

SqlEditorForm::SqlEditorForm(wb::WBContextSQLIDE *wbsql)
  : exec_sql_task(GrtThreadedTask::create()),
    _history(DbSqlEditorHistory::create()),
    _wbsql(wbsql),
    _version(grt::Initialized),
    _live_tree(SqlEditorTreeController::create(this)),
    _aux_dbc_conn(new sql::Dbc_connection_handler()),
    _usr_dbc_conn(new sql::Dbc_connection_handler()),
    _pimplMutex (new PrivateMutex) {
  _log = DbSqlEditorLog::create(this, 500);

  NotificationCenter::get()->add_observer(this, "GNApplicationActivated");
  NotificationCenter::get()->add_observer(this, "GNMainFormChanged");
  NotificationCenter::get()->add_observer(this, "GNFormTitleDidChange");
  NotificationCenter::get()->add_observer(this, "GNColorsChanged");
  GRTNotificationCenter::get()->add_grt_observer(this, "GRNServerStateChanged");
  exec_sql_task->desc("execute sql queries");
  exec_sql_task->send_task_res_msg(false);
  exec_sql_task->msg_cb(std::bind(&SqlEditorForm::add_log_message, this, std::placeholders::_1, std::placeholders::_2,
                                  std::placeholders::_3, ""));

  _last_log_message_timestamp = timestamp();

  long keep_alive_interval = bec::GRTManager::get()->get_app_option_int("DbSqlEditor:KeepAliveInterval", 600);

  if (keep_alive_interval != 0) {
    logDebug3("Creating KeepAliveInterval timer...\n");
    _keep_alive_task_id = ThreadedTimer::add_task(
      TimerTimeSpan, keep_alive_interval, false, std::bind(&SqlEditorForm::send_message_keep_alive_bool_wrapper, this));
  }

  _lower_case_table_names = 0;

  _continueOnError = (bec::GRTManager::get()->get_app_option_int("DbSqlEditor:ContinueOnError", 0) != 0);

  // set initial autocommit mode value
  _usr_dbc_conn->autocommit_mode = (bec::GRTManager::get()->get_app_option_int("DbSqlEditor:AutocommitMode", 1) != 0);

  _databaseSymbols.addDependencies({ &_staticServerSymbols });
}

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

SqlEditorForm::~SqlEditorForm() {
  if (_editorRefreshPending.connected())
    _editorRefreshPending.disconnect();

  if (_overviewRefreshPending.connected())
    _overviewRefreshPending.disconnect();

  // We need to remove it from cache, if not someone will be able to login without providing PW
  if (_connection.is_valid())
    mforms::Utilities::forget_cached_password(_connection->hostIdentifier(),
                                              _connection->parameterValues().get_string("userName"));

  delete _column_width_cache;

  // debug: ensure that close() was called when the tab is closed
  if (_toolbar != nullptr)
    logFatal("SqlEditorForm::close() was not called\n");

  NotificationCenter::get()->remove_observer(this);
  GRTNotificationCenter::get()->remove_grt_observer(this);

  delete _autosave_lock;
  _autosave_lock = 0;

  // Destructor can be called before the startup was finished.
  // On Windows the side palette is a child of the palette host and hence gets freed when we
  // free the host. On other platforms both are the same. In any case, double freeing it is
  // not a good idea.
  if (_side_palette_host != nullptr)
    _side_palette_host->release();

  delete _toolbar;
  delete _menu;

  reset_keep_alive_thread();
  _sshConnection.release();
}

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

void SqlEditorForm::cancel_connect() {
  _cancel_connect = true;
}

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

void SqlEditorForm::check_server_problems() {
  //_lower_case_table_names
  std::string compile_os;
  if (_usr_dbc_conn && get_session_variable(_usr_dbc_conn->ref.get(), "version_compile_os", compile_os)) {
    if ((_lower_case_table_names == 0 && (base::hasPrefix(compile_os, "Win") || base::hasPrefix(compile_os, "osx"))) ||
        (_lower_case_table_names == 2 && base::hasPrefix(compile_os, "Win")))
      mforms::Utilities::show_message_and_remember(
        _("Server Configuration Problems"),
        "A server configuration problem was detected.\nThe server is in a system that does not properly support the "
        "selected lower_case_table_names option value. Some problems may occur.\nPlease consult the MySQL server "
        "documentation.",
        _("OK"), "", "", "SQLIDE::check_server_problems::lower_case_table_names", "");
  }
}

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

void SqlEditorForm::finish_startup() {
  setup_side_palette();

  _live_tree->finish_init();

  std::string cache_dir = bec::GRTManager::get()->get_user_datadir() + "/cache/";
  try {
    base::create_directory(cache_dir, 0700); // No-op if the folder already exists.
  } catch (std::exception &e) {
    logError("Could not create %s: %s\n", cache_dir.c_str(), e.what());
  }

  _column_width_cache = new ColumnWidthCache(sanitize_file_name(get_session_name()), cache_dir);

  if (_usr_dbc_conn && !_usr_dbc_conn->active_schema.empty())
    _live_tree->on_active_schema_change(_usr_dbc_conn->active_schema);
  readStaticServerSymbols();

  bec::GRTManager::get()->run_once_when_idle(this, std::bind(&SqlEditorForm::update_menu_and_toolbar, this));

  this->check_server_problems();

  // We need to check this before sending GRNSQLEditorOpened cause offline() function that's called
  // from python which is connected to this notification will deadlock on PythonLock.
  checkIfOffline();

  // refresh snippets again, in case the initial load from DB is pending for shared snippets
  _side_palette->refresh_snippets();

  GRTNotificationCenter::get()->send_grt("GRNSQLEditorOpened", grtobj(), grt::DictRef());

  int keep_alive_interval = (int)bec::GRTManager::get()->get_app_option_int("DbSqlEditor:KeepAliveInterval", 600);

  //  We have to set these variables so that the server doesn't timeout before we ping everytime
  // From http://dev.mysql.com/doc/refman/5.7/en/communication-errors.html for reasones to loose the connection
  // - The client had been sleeping more than wait_timeout or interactive_timeout seconds without issuing any requests
  // to the server
  //  We're adding 10 seconds for communication delays
  {
    std::string value;

    if (get_session_variable(_usr_dbc_conn->ref.get(), "wait_timeout", value) &&
        base::atoi<int>(value) < keep_alive_interval)
      exec_main_sql(base::strfmt("SET @@SESSION.wait_timeout=%d", keep_alive_interval + 10), false);

    if (get_session_variable(_usr_dbc_conn->ref.get(), "interactive_timeout", value) &&
        base::atoi<int>(value) < keep_alive_interval)
      exec_main_sql(base::strfmt("SET @@SESSION.interactive_timeout=%d", keep_alive_interval + 10), false);
  }

  _startup_done = true;
}

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

base::RecMutexLock SqlEditorForm::getAuxConnection(sql::Dbc_connection_handler::Ref &conn, bool lockOnly) {
  RecMutexLock lock(ensure_valid_aux_connection(false, lockOnly));
  conn = _aux_dbc_conn;
  return lock;
}

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

base::RecMutexLock SqlEditorForm::getUserConnection(sql::Dbc_connection_handler::Ref &conn, bool lockOnly) {
  RecMutexLock lock(ensure_valid_usr_connection(false, lockOnly));
  conn = _usr_dbc_conn;
  return lock;
}

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

db_query_EditorRef SqlEditorForm::grtobj() {
  return wbsql()->get_grt_editor_object(this);
}

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

/**
 * Returns the name for this WQE instance derived from the connection it uses.
 * Used for workspace and action log.
 */
std::string SqlEditorForm::get_session_name() {
  if (_connection.is_valid()) {
    std::string name = _connection->name();
    if (name.empty())
      name = _connection->hostIdentifier();
    return name;
  }
  return "unconnected";
}

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

void SqlEditorForm::restore_last_workspace() {
  std::string name = get_session_name();
  if (!name.empty())
    load_workspace(sanitize_file_name(name));

  if (_tabdock->view_count() == 0)
    new_sql_scratch_area(false);

  // Gets the title for a NEW editor.
  _title = create_title();
  title_changed();
}

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

void SqlEditorForm::title_changed() {
  base::NotificationInfo info;
  info["form"] = form_id();
  info["title"] = _title;
  info["connection"] = _connection.is_valid() ? _connection->name() : "";
  base::NotificationCenter::get()->send("GNFormTitleDidChange", this, info);
}

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

void SqlEditorForm::handle_grt_notification(const std::string &name, grt::ObjectRef sender, grt::DictRef info) {
  if (name == "GRNServerStateChanged") {
    db_mgmt_ConnectionRef conn(db_mgmt_ConnectionRef::cast_from(info.get("connection")));

    ServerState new_state = UnknownState;
    if (info.get_int("state") == 1) {
      _serverIsOffline = false;
      new_state = RunningState;
    } else if (info.get_int("state") == -1) {
      _serverIsOffline = true;
      new_state = OfflineState;
    } else {
      _serverIsOffline = false;
      new_state = PossiblyStoppedState;
    }

    if (_last_server_running_state != new_state) {
      _last_server_running_state = new_state;
      if ((new_state == RunningState || new_state == OfflineState) && ping()) {
        // if new state is running but we're already connected, don't do anything
        return;
      }
      // reconnect when idle, to avoid any deadlocks
      if (conn.is_valid() && conn == connection_descriptor())
        bec::GRTManager::get()->run_once_when_idle(this, std::bind(&WBContextSQLIDE::reconnect_editor, wbsql(), this));
    }
  }
}

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

void SqlEditorForm::handle_notification(const std::string &name, void *sender, base::NotificationInfo &info) {
  if (name == "GNMainFormChanged") {
    if (_side_palette)
      _side_palette->close_popover();
    if (info["form"] == form_id())
      update_menu_and_toolbar();
  } else if (name == "GNFormTitleDidChange") {
    // Validates only if another editor to the same connection has sent the notification
    if (info["form"] != form_id() && _connection.is_valid() && _connection->name() == info["connection"]) {
      // This code is reached when at least 2 editors to the same host
      // have been opened, so the label of the old editor (which may not
      // contain the schema name should be updated with it).
      update_title();
    }
  } else if (name == "GNColorsChanged") {
    // Single colors or the entire color scheme changed.
    update_toolbar_icons();
  } else if (name == "GNApplicationActivated") {
    check_external_file_changes();
  }
}

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

void SqlEditorForm::reset_keep_alive_thread() {
  MutexLock keep_alive_thread_lock(_keep_alive_thread_mutex);
  if (_keep_alive_task_id) {
    ThreadedTimer::remove_task(_keep_alive_task_id);
    _keep_alive_task_id = 0;
  }
}

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

grt::StringRef SqlEditorForm::do_disconnect() {
  if (_usr_dbc_conn->ref.get()) {
    {
      RecMutexLock lock(_usr_dbc_conn_mutex);
      close_connection(_usr_dbc_conn);
      _usr_dbc_conn->ref.reset();
    }

    {
      RecMutexLock lock(_aux_dbc_conn_mutex);
      close_connection(_aux_dbc_conn);
      _aux_dbc_conn->ref.reset();
    }
  }

  return grt::StringRef();
}

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

void SqlEditorForm::close() {
  grt::ValueRef option(bec::GRTManager::get()->get_app_option("workbench:SaveSQLWorkspaceOnClose"));

  if (option.is_valid() && *grt::IntegerRef::cast_from(option)) {
    bec::GRTManager::get()->replace_status_text("Saving workspace state...");
    if (_autosave_path.empty()) {
      save_workspace(sanitize_file_name(get_session_name()), false);
      delete _autosave_lock;
    } else {
      auto_save();

      // Remove auto lock first or renaming the folder will fail.
      delete _autosave_lock;
      std::string new_name(base::strip_extension(_autosave_path) + ".workspace");
      int try_count = 0;

      // Rename our temporary workspace if one exists to make it a persistent one.
      if (base::file_exists(_autosave_path)) {
        for (;;) {
          try {
            if (base::file_exists(new_name))
              base::remove_recursive(new_name);
            base::rename(_autosave_path, new_name);
          } catch (base::file_error &err) {
            std::string path(dirname(_autosave_path));
            do {
              ++try_count;
              new_name =
                base::makePath(path, sanitize_file_name(get_session_name()).append(strfmt("-%i.workspace", try_count)));
            } while (file_exists(new_name));

            if (err.code() == base::already_exists)
              continue;
            logWarning("Could not rename autosave directory %s: %s\n", _autosave_path.c_str(), err.what());
          }

          break;
        }
      }
    }
    _autosave_lock = 0;
  } else {
    delete _autosave_lock;
    _autosave_lock = 0;
    if (!_autosave_path.empty())
      base_rmdir_recursively(_autosave_path.c_str());
  }

  // Ensure all processing is stopped before freeing the info structure, otherwise references
  // are kept that prevent the correct deletion of the editor.
  if (_tabdock) {
    for (std::size_t c = _tabdock->view_count(), i = 0; i < c; i++) {
      SqlEditorPanel *p = sql_editor_panel((int)i);
      if (p)
        p->editor_be()->stop_processing();
    }

    _closing = true;
    _tabdock->close_all_views();
    _closing = false;
  }
  bec::GRTManager::get()->replace_status_text("Closing SQL Editor...");
  wbsql()->editor_will_close(this);

  exec_sql_task->exec(true, std::bind(&SqlEditorForm::do_disconnect, this));
  exec_sql_task->disconnect_callbacks();
  reset_keep_alive_thread();
  bec::GRTManager::get()->replace_status_text("SQL Editor closed");

  delete _menu;
  _menu = nullptr;
  delete _toolbar;
  _toolbar = nullptr;
}

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

std::string SqlEditorForm::get_form_context_name() const {
  return WB_CONTEXT_QUERY;
}

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

db_mgmt_SSHConnectionRef SqlEditorForm::getSSHConnection() {
  try {
    if (!_sshConnection.is_valid()) {
      if (_connection.is_valid()) {
        auto val = getServerInstance(_connection);
        if (val.is_valid()) {
          db_mgmt_SSHConnectionRef object(grt::Initialized);
          object->owner(wb::WBContextUI::get()->get_wb()->get_root());
          object->name(_connection->name());
          object->set_data(new ssh::SSHSessionWrapper(val));
          _sshConnection = object;
        }
      }
    }
  } catch (std::runtime_error &) {
    logError("Unable to create db_mgmt_SSHConnectionRef object\n");
  }
  return _sshConnection;
}

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

bool SqlEditorForm::get_session_variable(sql::Connection *dbc_conn, const std::string &name, std::string &value) {
  if (dbc_conn) {
    SqlFacade::Ref sql_facade = SqlFacade::instance_for_rdbms(rdbms());
    Sql_specifics::Ref sql_specifics = sql_facade->sqlSpecifics();
    std::string query = sql_specifics->query_variable(name);
    if (query.empty())
      return false;

    const std::unique_ptr<sql::Statement> statement(dbc_conn->createStatement());
    const std::unique_ptr<sql::ResultSet> rs(statement->executeQuery(query));
    if (rs->next()) {
      value = rs->getString(2);
      return true;
    }
  }
  return false;
}

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

void SqlEditorForm::schema_tree_did_populate() {
  if (!_pending_expand_nodes.empty() &&
      bec::GRTManager::get()->get_app_option_int("DbSqlEditor:SchemaTreeRestoreState", 1)) {
    std::string schema, groups;
    base::partition(_pending_expand_nodes, ":", schema, groups);

    mforms::TreeNodeRef node =
      _live_tree->get_schema_tree()->get_node_for_object(schema, wb::LiveSchemaTree::Schema, "");
    if (node) {
      static const char *nodes[] = {"tables", "views", "procedures", "functions", nullptr};
      node->expand();
      for (int i = 0; nodes[i]; i++)
        if (strstr(groups.c_str(), nodes[i])) {
          mforms::TreeNodeRef child = node->get_child(i);
          if (child)
            child->expand();
        }
    }
    _pending_expand_nodes.clear();
  }
}

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

std::string SqlEditorForm::fetch_data_from_stored_procedure(std::string proc_call,
                                                            std::shared_ptr<sql::ResultSet> &rs) {
  std::string ret_val("");
  try {
    RecMutexLock aux_dbc_conn_mutex(ensure_valid_aux_connection());
    std::unique_ptr<sql::Statement> stmt(_aux_dbc_conn->ref->createStatement());
    stmt->execute(std::string(proc_call));
    do {
      rs.reset(stmt->getResultSet());
    } while (stmt->getMoreResults());
  } catch (const sql::SQLException &exc) {
    logWarning("Error retrieving data from stored procedure '%s': Error %d : %s", proc_call.c_str(), exc.getErrorCode(),
               exc.what());
    ret_val = base::strfmt("MySQL Error : %s (code %d)", exc.what(), exc.getErrorCode());
  }

  return ret_val;
}

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

void SqlEditorForm::update_sql_mode_for_editors() {
  for (int c = sql_editor_count(), i = 0; i < c; i++) {
    SqlEditorPanel *panel = sql_editor_panel(i);
    if (panel)
      panel->editor_be()->set_sql_mode(_sql_mode);
  }
}

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

void SqlEditorForm::cache_sql_mode() {
  std::string sql_mode;
  if (_usr_dbc_conn && get_session_variable(_usr_dbc_conn->ref.get(), "sql_mode", sql_mode)) {
    if (sql_mode != _sql_mode) {
      _sql_mode = sql_mode;
      bec::GRTManager::get()->run_once_when_idle(this, std::bind(&SqlEditorForm::update_sql_mode_for_editors, this));
    }
  }
}

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

void SqlEditorForm::query_ps_statistics(std::int64_t conn_id, std::map<std::string, std::int64_t> &stats) {
  static const char *stat_fields[] = {"EVENT_ID",
                                      "THREAD_ID",
                                      "TIMER_WAIT",
                                      "LOCK_TIME",
                                      "ERRORS",
                                      "WARNINGS",
                                      "ROWS_AFFECTED",
                                      "ROWS_SENT",
                                      "ROWS_EXAMINED",
                                      "CREATED_TMP_DISK_TABLES",
                                      "CREATED_TMP_TABLES",
                                      "SELECT_FULL_JOIN",
                                      "SELECT_FULL_RANGE_JOIN",
                                      "SELECT_RANGE",
                                      "SELECT_RANGE_CHECK",
                                      "SELECT_SCAN",
                                      "SORT_MERGE_PASSES",
                                      "SORT_RANGE",
                                      "SORT_ROWS",
                                      "SORT_SCAN",
                                      "NO_INDEX_USED",
                                      "NO_GOOD_INDEX_USED",
                                      nullptr};
  RecMutexLock lock(ensure_valid_aux_connection());

  std::unique_ptr<sql::Statement> stmt(_aux_dbc_conn->ref->createStatement());

  try {
    std::unique_ptr<sql::ResultSet> result(stmt->executeQuery(base::strfmt(
      "SELECT st.* FROM performance_schema.events_statements_current st JOIN performance_schema.threads thr"
      " ON thr.thread_id = st.thread_id WHERE thr.processlist_id = %lli",
      (long long int)conn_id)));
    while (result->next()) {
      for (const char **field = stat_fields; *field; ++field) {
        stats[*field] = result->getInt64(*field);
      }
    }
  } catch (sql::SQLException &exc) {
    logException("Error querying performance_schema.events_statements_current\n", exc);
  }
}

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

std::vector<SqlEditorForm::PSStage> SqlEditorForm::query_ps_stages(std::int64_t stmt_event_id) {
  RecMutexLock lock(ensure_valid_aux_connection());

  std::unique_ptr<sql::Statement> stmt(_aux_dbc_conn->ref->createStatement());
  std::vector<PSStage> stages;
  try {
    std::unique_ptr<sql::ResultSet> result(stmt->executeQuery(
      base::strfmt("SELECT st.* FROM performance_schema.events_stages_history_long st WHERE st.nesting_event_id = %lli",
                   (long long int)stmt_event_id)));
    while (result->next()) {
      double wait_time = (double)result->getInt64("TIMER_WAIT") / 1000000000.0; // ps to ms
      std::string event = result->getString("EVENT_NAME");

      // rename the stage/sql/Sending data event to something more suitable
      if (event == "stage/sql/Sending data")
        event = "executing (storage engine)";

      bool flag = false;
      for (std::vector<PSStage>::iterator iter = stages.begin(); iter != stages.end(); ++iter) {
        if (iter->name == event) {
          flag = true;
          iter->wait_time += wait_time;
          break;
        }
      }

      if (!flag) {
        PSStage stage;
        stage.name = event;
        stage.wait_time = wait_time;
        stages.push_back(stage);
      }
    }
  } catch (sql::SQLException &exc) {
    logException("Error querying performance_schema.event_stages_history\n", exc);
  }

  return stages;
}

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

std::vector<SqlEditorForm::PSWait> SqlEditorForm::query_ps_waits(std::int64_t stmt_event_id) {
  RecMutexLock lock(ensure_valid_aux_connection());

  std::unique_ptr<sql::Statement> stmt(_aux_dbc_conn->ref->createStatement());
  std::vector<PSWait> waits;
  try {
    std::unique_ptr<sql::ResultSet> result(stmt->executeQuery(
      base::strfmt("SELECT st.* FROM performance_schema.events_waits_history_long st WHERE st.nesting_event_id = %lli",
                   (long long int)stmt_event_id)));
    while (result->next()) {
      double wait_time = (double)result->getInt64("TIMER_WAIT") / 1000000000.0; // ps to ms
      std::string event = result->getString("EVENT_NAME");
      bool flag = false;
      for (std::vector<PSWait>::iterator iter = waits.begin(); iter != waits.end(); ++iter) {
        if (iter->name == event) {
          flag = true;
          iter->wait_time += wait_time;
          break;
        }
      }

      if (!flag) {
        PSWait wait;
        wait.name = event;
        wait.wait_time = wait_time;
        waits.push_back(wait);
      }
    }
  } catch (sql::SQLException &exc) {
    logException("Error querying performance_schema.event_waits_history\n", exc);
  }
  return waits;
}

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

SqlEditorPanel *SqlEditorForm::run_sql_in_scratch_tab(const std::string &sql, bool reuse_if_possible,
                                                      bool start_collapsed) {
  SqlEditorPanel *editor;

  if (!(editor = active_sql_editor_panel()) || !reuse_if_possible || !editor->is_scratch())
    editor = new_sql_scratch_area(start_collapsed);
  editor->editor_be()->get_editor_control()->set_text(sql.c_str());
  run_editor_contents(false);
  editor->editor_be()->get_editor_control()->reset_dirty();

  return editor;
}

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

void SqlEditorForm::reset() {
  SqlEditorPanel *panel = active_sql_editor_panel();
  if (panel)
    panel->editor_be()->cancel_auto_completion();
}

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

void logToWorkbenchLog(int messageType, std::string const &msg) {
  switch (messageType) {
    case DbSqlEditorLog::ErrorMsg:
      logError("%s\n", msg.c_str());
      break;

    case DbSqlEditorLog::WarningMsg:
      logWarning("%s\n", msg.c_str());
      break;

    case DbSqlEditorLog::NoteMsg:
      logDebug("%s\n", msg.c_str());
      break;

    case DbSqlEditorLog::OKMsg:
      logDebug2("%s\n", msg.c_str());
      break;

    case DbSqlEditorLog::BusyMsg:
      logDebug3("%s\n", msg.c_str());
      break;
  }
}

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

int SqlEditorForm::add_log_message(int messageType, const std::string &msg, const std::string &context,
                                   const std::string &duration) {
  RowId new_log_message_index = _log->add_message(messageType, context, msg, duration);
  _has_pending_log_messages = true;
  refresh_log_messages(false);
  if (messageType == DbSqlEditorLog::ErrorMsg || messageType == DbSqlEditorLog::WarningMsg)
    _exec_sql_error_count++;

  logToWorkbenchLog(messageType, msg);
  return (int)new_log_message_index;
}

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

void SqlEditorForm::set_log_message(RowId log_message_index, int messageType, const std::string &msg,
                                    const std::string &context, const std::string &duration) {
  if (log_message_index != (RowId)-1) {
    _log->set_message(log_message_index, messageType, context, msg, duration);
    _has_pending_log_messages = true;
    if (messageType == DbSqlEditorLog::ErrorMsg || messageType == DbSqlEditorLog::WarningMsg)
      ++_exec_sql_error_count;
    refresh_log_messages(messageType == DbSqlEditorLog::BusyMsg); // Force refresh only for busy messages.
  }

  logToWorkbenchLog(messageType, msg);
}

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

void SqlEditorForm::refresh_log_messages(bool ignore_last_message_timestamp) {
  if (_has_pending_log_messages) {
    bool is_refresh_needed = ignore_last_message_timestamp;
    if (!ignore_last_message_timestamp) {
      double now = timestamp();
      int progress_status_update_interval =
        (int)(bec::GRTManager::get()->get_app_option_int("DbSqlEditor:ProgressStatusUpdateInterval", 500) / 1000.);

      if (_last_log_message_timestamp + progress_status_update_interval < now)
        is_refresh_needed = true;
      _last_log_message_timestamp = now;
    }
    if (is_refresh_needed) {
      _log->refresh();
      _has_pending_log_messages = false;
    }
  }
}

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

void SqlEditorForm::init_connection(sql::Connection *dbc_conn_ref, const db_mgmt_ConnectionRef &connectionProperties,
                                    sql::Dbc_connection_handler::Ref &dbc_conn, bool user_connection) {
  db_mgmt_RdbmsRef rdbms = db_mgmt_RdbmsRef::cast_from(_connection->driver()->owner());
  SqlFacade::Ref sql_facade = SqlFacade::instance_for_rdbms(rdbms);
  Sql_specifics::Ref sql_specifics = sql_facade->sqlSpecifics();

  // connection startup script
  {
    std::list<std::string> sql_script;
    {
      sql_specifics->get_connection_startup_script(sql_script);
      bool use_ansi_quotes = (connectionProperties->parameterValues().get_int("useAnsiQuotes", 0) != 0);
      if (use_ansi_quotes) {
        std::string sql = sql_specifics->setting_ansi_quotes();
        if (!sql.empty())
          sql_script.push_back(sql);
      }
    }

    // check if SQL_SAFE_UPDATES should be enabled (only for user connections, don't do it for the aux connection)
    if (bec::GRTManager::get()->get_app_option_int("DbSqlEditor:SafeUpdates", 1) && user_connection)
      sql_script.push_back("SET SQL_SAFE_UPDATES=1");

    std::unique_ptr<sql::Statement> stmt(dbc_conn_ref->createStatement());
    sql::SqlBatchExec sql_batch_exec;
    sql_batch_exec(stmt.get(), sql_script);

    if (!user_connection) {
      std::string sql_mode;
      if (get_session_variable(dbc_conn_ref, "sql_mode", sql_mode) && sql_mode.find("MYSQL40") != std::string::npos) {
        // MYSQL40 used CREATE TABLE ... TYPE=<engine> instead of ENGINE=<engine>, which is not supported by our reveng
        // code
        std::vector<std::string> options(base::split(sql_mode, ","));
        for (std::vector<std::string>::iterator i = options.begin(); i != options.end(); ++i) {
          if (*i == "MYSQL40") {
            options.erase(i);
            break;
          }
        }

        std::unique_ptr<sql::Statement> stmt(dbc_conn_ref->createStatement());
        std::string query = base::sqlstring("SET SESSION SQL_MODE=?", 0) << base::join(options, ",");
        stmt->execute(query);
      }
    }
  }

  // remember connection id
  {
    std::string query_connection_id = sql_specifics->query_connection_id();
    if (!query_connection_id.empty()) {
      std::unique_ptr<sql::Statement> stmt(dbc_conn_ref->createStatement());
      stmt->execute(query_connection_id);
      std::shared_ptr<sql::ResultSet> rs(stmt->getResultSet());
      rs->next();
      dbc_conn->id = rs->getInt(1);
    }
  }
}

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

static void set_active_schema(SqlEditorForm::Ptr self, const std::string &schema) {
  SqlEditorForm::Ref ed(self.lock());
  if (ed)
    ed->active_schema(schema);
}

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

void SqlEditorForm::create_connection(sql::Dbc_connection_handler::Ref &dbc_conn, db_mgmt_ConnectionRef db_mgmt_conn,
                                      std::shared_ptr<SSHTunnel> tunnel, sql::Authentication::Ref auth,
                                      bool autocommit_mode, bool user_connection) {
  dbc_conn->is_stop_query_requested = false;

  sql::DriverManager *dbc_drv_man = sql::DriverManager::getDriverManager();

  db_mgmt_ConnectionRef temp_connection = db_mgmt_ConnectionRef::cast_from(grt::CopyContext().copy(db_mgmt_conn));

  int read_timeout = (int)bec::GRTManager::get()->get_app_option_int("DbSqlEditor:ReadTimeOut");

  if (read_timeout < 0) {
      bec::GRTManager::get()->set_app_option("DbSqlEditor:ReadTimeOut", grt::IntegerRef((int)0));
      read_timeout = 0;
  }

  temp_connection->parameterValues().set("OPT_READ_TIMEOUT", grt::IntegerRef(read_timeout));

  int connect_timeout = (int)bec::GRTManager::get()->get_app_option_int("DbSqlEditor:ConnectionTimeOut");
  if (connect_timeout > 0)
    temp_connection->parameterValues().set("OPT_CONNECT_TIMEOUT", grt::IntegerRef(connect_timeout));

  temp_connection->parameterValues().set("CLIENT_INTERACTIVE", grt::IntegerRef(1));

  try {
    dbc_conn->ref = dbc_drv_man->getConnection(temp_connection, tunnel, auth,
                                               std::bind(&SqlEditorForm::init_connection, this, std::placeholders::_1,
                                                         std::placeholders::_2, dbc_conn, user_connection));

    note_connection_open_outcome(0); // success
  } catch (sql::SQLException &exc) {
    note_connection_open_outcome(exc.getErrorCode());
    throw;
  }

  //! dbms-specific code
  if (dbc_conn->ref->getMetaData()->getDatabaseMajorVersion() < 5) {
    throw std::runtime_error("MySQL Server version is older than 5.x, which is not supported");
  }

  // get SSL enabled info
  {
    std::unique_ptr<sql::Statement> stmt(dbc_conn->ref->createStatement());
    std::unique_ptr<sql::ResultSet> result(stmt->executeQuery("SHOW SESSION STATUS LIKE 'Ssl_cipher'"));
    if (result->next()) {
      dbc_conn->ssl_cipher = result->getString(2);
    }
  }

  // Activate default schema, if it's empty, use last active
  if (dbc_conn->active_schema.empty()) {
    std::string default_schema = temp_connection->parameterValues().get_string("schema");

    if (default_schema.empty())
      default_schema = temp_connection->parameterValues().get_string("DbSqlEditor:LastDefaultSchema");
    if (!default_schema.empty()) {
      try {
        dbc_conn->ref->setSchema(default_schema);
        dbc_conn->active_schema = default_schema;

        bec::GRTManager::get()->run_once_when_idle(this,
                                                   std::bind(&set_active_schema, shared_from_this(), default_schema));
      } catch (std::exception &exc) {
        logError("Can't restore default schema (%s): %s\n", default_schema.c_str(), exc.what());
        temp_connection->parameterValues().gset("DbSqlEditor:LastDefaultSchema", "");
      }
    }
  } else
    dbc_conn->ref->setSchema((dbc_conn->active_schema));

  dbc_conn->ref->setAutoCommit(autocommit_mode);
  dbc_conn->autocommit_mode = dbc_conn->ref->getAutoCommit();
}

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

struct ConnectionErrorInfo {
  sql::AuthenticationError *auth_error;
  bool password_expired;
  bool server_probably_down;
  bool serverIsOffline;
  grt::server_denied *serverException;

  ConnectionErrorInfo()
    : auth_error(nullptr),
      password_expired(false),
      server_probably_down(false),
      serverIsOffline(false),
      serverException(nullptr) {
  }
  ~ConnectionErrorInfo() {
    delete auth_error;
    delete serverException;
  }
};

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

void SqlEditorForm::set_connection(db_mgmt_ConnectionRef conn) {
  if (_connection.is_valid())
    logWarning("Setting connection on an editor with a connection already set\n");

  _connection = conn;

  _dbc_auth = sql::Authentication::create(_connection, "");

  // initialize the password with a cached value
  {
    std::string password = conn->parameterValues().get_string("password");
    bool ok = true;
    if (password.empty()) {
      if (!mforms::Utilities::find_password(conn->hostIdentifier(), conn->parameterValues().get_string("userName"),
                                            password)) {
        if (!mforms::Utilities::find_cached_password(conn->hostIdentifier(),
                                                     conn->parameterValues().get_string("userName"), password)) {
          ok = false;
        }
      }
    }
    if (ok)
      _dbc_auth->set_password(password.c_str());
  }

  // send editor open notification again, in case the connection is being set after the connection
  // tab is opened. this will be caught by the admin code to init itself
  if (_startup_done)
    GRTNotificationCenter::get()->send_grt("GRNSQLEditorOpened", grtobj(), grt::DictRef());
}

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

bool SqlEditorForm::connect(std::shared_ptr<SSHTunnel> tunnel) {
  sql::Authentication::Ref auth = _dbc_auth; // sql::Authentication::create(_connection, "");
  enum PasswordMethod { NoPassword, KeychainPassword, InteractivePassword } current_method = NoPassword;

  reset();

  // In the 1st connection attempt, no password is supplied
  // If it fails, keychain is checked and used if it exists
  // If it fails, an interactive password request is made

  // connect
  for (;;) {
    // if an error happens in the worker thread, this ptr will be set
    ConnectionErrorInfo error_ptr;

    // connection must happen in the worker thread
    try {
      exec_sql_task->exec(true, std::bind(&SqlEditorForm::do_connect, this, tunnel, auth, &error_ptr));

      // check if user cancelled
      if (_cancel_connect) // return false, so it looks like the server is down
      {
        close();
        return false;
      }
    } catch (grt::grt_runtime_error &) {
      if (error_ptr.serverException != nullptr)
        throw grt::server_denied(*error_ptr.serverException);

      if (error_ptr.password_expired)
        throw std::runtime_error(":PASSWORD_EXPIRED");

      if (!error_ptr.auth_error)
        throw;
      else if (error_ptr.server_probably_down || error_ptr.serverIsOffline)
        return false;

      // check if user cancelled
      if (_cancel_connect) // return false, so it looks like the server is down
      {
        close();
        return false;
      }

      if (current_method == NoPassword) {
        // lookup in keychain
        std::string pwd;
        if (sql::DriverManager::getDriverManager()->findStoredPassword(auth->connectionProperties(), pwd)) {
          auth->set_password(pwd.c_str());
          current_method = KeychainPassword;
        } else {
          // not in keychain, go straight to interactive
          pwd = sql::DriverManager::getDriverManager()->requestPassword(auth->connectionProperties(), true);
          auth->set_password(pwd.c_str());
          current_method = InteractivePassword;
        }
      } else if (current_method == KeychainPassword) {
        // now try interactive
        std::string pwd = sql::DriverManager::getDriverManager()->requestPassword(auth->connectionProperties(), true);
        auth->set_password(pwd.c_str());
      } else // if interactive failed, pass the exception higher up to be displayed to the user
        throw;
      continue;
    }
    break;
  }

  // XXX: ouch, what if we ever change the init sequence, *nobody* will look here to note the side effect.
  // we should only send this after the initial connection
  // assumes setup_side_palette() is called in finish_init(), signalizing that the editor was already initialized once
  if (_side_palette) // we're in a thread here, so make sure the notification is sent from the main thread
  {
    bec::GRTManager::get()->run_once_when_idle(this, std::bind(&SqlEditorForm::update_connected_state, this));
  }

  return true;
}

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

void SqlEditorForm::update_connected_state() {
  grt::DictRef args(true);
  args.gset("connected", connected());
  GRTNotificationCenter::get()->send_grt("GRNSQLEditorReconnected", grtobj(), args);

  update_menu_and_toolbar();
}

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

std::string SqlEditorForm::get_client_lib_version() {
  std::string version;
  sql::DriverManager *dbc_driver_man = sql::DriverManager::getDriverManager();
  if (dbc_driver_man != nullptr)
    version = dbc_driver_man->getClientLibVersion();
  return version;
}

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

/**
 * Formats a single text line for the connection info output.
 */
std::string createTableRow(const std::string &name, const std::string &value = "") {
  if (value.empty()) // Empty value means: heading row.
    return "<tr class='heading'>"s +
      "<td style='border:none; padding-left: 0px;' colspan=2>" + name + "</td>"
      "</tr>";

  return "<tr>"s +
    "<td style='border:none; padding-left: 15px;'>" + name + "</td>"
    "<td style='border:none;'>" + value + "</td>"
    "</tr>";

}

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

grt::StringRef SqlEditorForm::do_connect(std::shared_ptr<SSHTunnel> tunnel, sql::Authentication::Ref &auth,
                                         ConnectionErrorInfo *err_ptr) {
  try {
    RecMutexLock aux_dbc_conn_mutex(_aux_dbc_conn_mutex);
    RecMutexLock usr_dbc_conn_mutex(_usr_dbc_conn_mutex);

    _aux_dbc_conn->ref.reset();
    _usr_dbc_conn->ref.reset();

    _connection_details["name"] = _connection->name();
    _connection_details["hostName"] = _connection->parameterValues().get_string("hostName");
    _connection_details["port"] = strfmt("%li\n", (long)_connection->parameterValues().get_int("port"));

    _connection_details["socket"] = _connection->parameterValues().get_string("socket");
    _connection_details["driverName"] = _connection->driver()->name();
    _connection_details["userName"] = _connection->parameterValues().get_string("userName");

    // During the connection process create also a description about the connection details that can be shown
    // in the SQL IDE. Only the body content is created here. Surrounding code is added by the tree controller.
    // Start with the default content, in case we cannot connect.
    _connectionInfo = "<b><span style='color: red'>No connection established</span></b>";

    std::string newConnectionInfo = "<body><table style='border: none; border-collapse: collapse;'>" +
       createTableRow("Connection Details") + createTableRow("Name: ", _connection->name());

    _tunnel = tunnel;
    if (_tunnel == nullptr) {
      if (_connection->driver()->name() == "MysqlNativeSocket") {
      #ifdef _MSC_VER
        std::string name = _connection->parameterValues().get_string("socket", "");
        if (name.empty())
          name = "pipe";
      #else
        std::string name = _connection->parameterValues().get_string("socket", "");
        if (name.empty())
          name = "UNIX socket";
      #endif

        newConnectionInfo += createTableRow("Host:", "localhost (" + name + ")");
      } else {
        newConnectionInfo += createTableRow("Host:", _connection->parameterValues().get_string("hostName"));
        newConnectionInfo += createTableRow("Port:", std::to_string(_connection->parameterValues().get_int("port")));
      }
    }

    // open connections
    create_connection(_aux_dbc_conn, _connection, tunnel, auth, _aux_dbc_conn->autocommit_mode, false);
    create_connection(_usr_dbc_conn, _connection, tunnel, auth, _usr_dbc_conn->autocommit_mode, true);
    _serverIsOffline = false;
    cache_sql_mode();

    try {
      std::string value;
      get_session_variable(_usr_dbc_conn->ref.get(), "version_comment", value);
      _connection_details["dbmsProductName"] = value;
      get_session_variable(_usr_dbc_conn->ref.get(), "version", value);
      _connection_details["dbmsProductVersion"] = value;

      logInfo("Opened connection '%s' to %s version %s\n", _connection->name().c_str(),
        _connection_details["dbmsProductName"].c_str(), _connection_details["dbmsProductVersion"].c_str());

      _version = parse_version(_connection_details["dbmsProductVersion"]);
      _version->name(grt::StringRef(_connection_details["dbmsProductName"]));

      db_query_EditorRef editor(grtobj());
      if (editor.is_valid()) // this will be valid only on reconnections
        editor->serverVersion(_version);

      newConnectionInfo += createTableRow("Login User:", _connection->parameterValues().get_string("userName"));

      // check the actual user we're logged in as
      if (_usr_dbc_conn && _usr_dbc_conn->ref.get()) {
        const std::unique_ptr<sql::Statement> statement(_usr_dbc_conn->ref->createStatement());
        const std::unique_ptr<sql::ResultSet> rs(statement->executeQuery("SELECT current_user()"));
        if (rs->next())
          newConnectionInfo += createTableRow("Current User:", rs->getString(1));
      }

      newConnectionInfo += createTableRow(
        "SSL cipher:", _usr_dbc_conn->ssl_cipher.empty() ? "SSL not used" : _usr_dbc_conn->ssl_cipher);

      if (_tunnel != nullptr) {
        auto &config = _tunnel->getConfig();
        newConnectionInfo += createTableRow("SSH Tunnel");
        newConnectionInfo += createTableRow("Target:", config.remoteSSHhost + ":" + std::to_string(config.remoteSSHport));
        newConnectionInfo += createTableRow("Local port:", std::to_string(config.localport));
        newConnectionInfo += createTableRow("Remote port:", std::to_string(config.remoteport));
        newConnectionInfo += createTableRow("Remote host:", config.remotehost);
        newConnectionInfo += createTableRow("Config file:", config.configFile);
      }

      newConnectionInfo += createTableRow("Server");
      newConnectionInfo += createTableRow("Product:", _connection_details["dbmsProductName"]);
      newConnectionInfo += createTableRow("Version:", _connection_details["dbmsProductVersion"]);

      newConnectionInfo += createTableRow("Connector");
      newConnectionInfo += createTableRow("Version:", get_client_lib_version());

      if (_usr_dbc_conn && get_session_variable(_usr_dbc_conn->ref.get(), "lower_case_table_names", value))
        _lower_case_table_names = base::atoi<int>(value, 0);

      parsers::MySQLParserServices::Ref services = parsers::MySQLParserServices::get();
      _work_parser_context =
        services->createParserContext(rdbms()->characterSets(), _version, _sql_mode, _lower_case_table_names != 0);

      // If we came so far, we probably have a valid connection.
      _connectionInfo = newConnectionInfo + "</table>";
    }
    CATCH_ANY_EXCEPTION_AND_DISPATCH(_("Get connection information"));
  } catch (sql::AuthenticationError &authException) {
    err_ptr->auth_error = new sql::AuthenticationError(authException);
    throw;
  } catch (sql::SQLException &exc) {
    logException("SqlEditorForm: exception in do_connect method", exc);

    _version = bec::intToVersion(50717); // Set a meaningful default version if we cannot open a connection.

    switch (exc.getErrorCode()) {
      case 1820: // ER_MUST_CHANGE_PASSWORD_LOGIN
        err_ptr->password_expired = true;
        break;

      case 2013:
      case 2003:
      case 2002: { // ERROR 2003 (HY000): Can't connect to MySQL server on X.Y.Z.W (or via socket)
        add_log_message(WarningMsg, exc.what(), "Could not connect, server may not be running.", "");

        err_ptr->server_probably_down = true;

        if (_connection.is_valid()) {
          // if there's no connection, then we continue anyway if this is a local connection or
          // a remote connection with remote admin enabled..
          grt::Module *m = grt::GRT::get()->get_module("WbAdmin");
          grt::BaseListRef args(true);
          args.ginsert(_connection);
          if (!m || *grt::IntegerRef::cast_from(m->call_function("checkConnectionForRemoteAdmin", args)) == 0) {
            logError("Connection failed but remote admin does not seem to be available, rethrowing exception...\n");
            throw;
          }
          logInfo("Error %i connecting to server, assuming server is down and opening editor with no connection\n",
                  exc.getErrorCode());
        }

        logInfo("Error %i connecting to server, assuming server is down and opening editor with no connection\n",
                exc.getErrorCode());

        // Create a parser with some sensible defaults if we cannot connect.
        // We specify no charsets here, disabling parsing of repertoires.
        parsers::MySQLParserServices::Ref services = parsers::MySQLParserServices::get();
        _work_parser_context = services->createParserContext(GrtCharacterSetsRef(true), _version, _sql_mode, true);
        return grt::StringRef();
      }

      case 3032: {
        err_ptr->serverIsOffline = true;
        _serverIsOffline = true;
        add_log_message(WarningMsg, exc.what(), "Could not connect, server is in offline mode.", "");

        if (_connection.is_valid()) {
          // if there's no connection, then we continue anyway if this is a local connection or
          // a remote connection with remote admin enabled..
          grt::GRT::get()->get_module("WbAdmin");
          grt::BaseListRef args(true);
          args.ginsert(_connection);
        }

        logInfo("Error %i connecting to server, server is in offline mode. Only superuser connection are allowed. "
                "Opening editor with no connection\n", exc.getErrorCode());

        // Create a parser with some sensible defaults if we cannot connect.
        // We specify no charsets here, disabling parsing of repertoires.
        parsers::MySQLParserServices::Ref services = parsers::MySQLParserServices::get();
        _work_parser_context = services->createParserContext(GrtCharacterSetsRef(true), _version, _sql_mode, true);

        return grt::StringRef();
      }

      case 3159: {
        // require SSL, offline mode
        // we need to change exception type so we can properly handle it in
        err_ptr->serverException = new grt::server_denied(exc.what(), exc.getErrorCode());
        break;
      }
    }

    _connectionInfo += "</body>";
    throw;
  }

  _connectionInfo += "</body>";
  
  return grt::StringRef();
}

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

bool SqlEditorForm::connected() const {
  // If the conn mutex is locked by someone else, then we assume the conn is in use and thus,
  // there's a connection.
  bool busy = !_usr_dbc_conn_mutex.tryLock();
  if (!busy)
    _usr_dbc_conn_mutex.unlock();
  if (_usr_dbc_conn && (busy || _usr_dbc_conn->ref.get_ptr()))
    return true; // we don't need to PING the server every time we want to check if the editor is connected
  return false;
}

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

void SqlEditorForm::checkIfOffline() {
  bool locked = _usr_dbc_conn_mutex.tryLock();
  size_t counter = 1;
  while (!locked) {
    if (counter >= 30) {
      logError("Can't lock conn mutex for 30 seconds, assuming server is not offline.\n");
      break;
    }

    logDebug3("Can't lock connection mutex, trying again in one sec.\n");
    std::this_thread::sleep_for(std::chrono::seconds(1));
    counter++;
    locked = _usr_dbc_conn_mutex.tryLock();
  }

  if (locked) {
    std::string result;
    if (_usr_dbc_conn && get_session_variable(_usr_dbc_conn->ref.get(), "offline_mode", result)) {
      if (base::string_compare(result, "ON") == 0)
        _serverIsOffline = true;
    }

    _usr_dbc_conn_mutex.unlock();
  }
}

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

bool SqlEditorForm::offline() {
  if (_serverIsOffline)
    return true;

  if (!connected())
    return false;

  return _serverIsOffline;
}

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

bool SqlEditorForm::ping() const {
  // If the conn mutex is locked by someone else, then we assume the conn is in use and thus,
  // there's a connection.
  bool locked = _usr_dbc_conn_mutex.tryLock();
  if (!locked)
    return true;

  if (_usr_dbc_conn && _usr_dbc_conn->ref.get_ptr()) {
    std::unique_ptr<sql::Statement> stmt(_usr_dbc_conn->ref->createStatement());
    try {
      stmt->execute("do 1");
      _usr_dbc_conn_mutex.unlock();
      return true;
    } catch (const std::exception &ex) {
      logError("Failed to ping the server: %s\n", ex.what());
    }
  }

  _usr_dbc_conn_mutex.unlock();

  return false;
}

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

base::RecMutexLock SqlEditorForm::ensure_valid_aux_connection(sql::Dbc_connection_handler::Ref &conn, bool lockOnly) {
  RecMutexLock lock(ensure_valid_dbc_connection(_aux_dbc_conn, _aux_dbc_conn_mutex, lockOnly));
  conn = _aux_dbc_conn;
  return lock;
}

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

RecMutexLock SqlEditorForm::ensure_valid_aux_connection(bool throw_on_block, bool lockOnly) {
  return ensure_valid_dbc_connection(_aux_dbc_conn, _aux_dbc_conn_mutex, throw_on_block, lockOnly);
}

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

RecMutexLock SqlEditorForm::ensure_valid_usr_connection(bool throw_on_block, bool lockOnly) {
  return ensure_valid_dbc_connection(_usr_dbc_conn, _usr_dbc_conn_mutex, throw_on_block, lockOnly);
}

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

void SqlEditorForm::close_connection(sql::Dbc_connection_handler::Ref &dbc_conn) {
  sql::Dbc_connection_handler::Ref myref(dbc_conn);
  if (dbc_conn && dbc_conn->ref.get_ptr()) {
    try {
      dbc_conn->ref->close();
    } catch (sql::SQLException &) {
      // ignore if the connection is already closed
    }
  }
}

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

RecMutexLock SqlEditorForm::ensure_valid_dbc_connection(sql::Dbc_connection_handler::Ref &dbc_conn,
                                                        base::RecMutex &dbc_conn_mutex, bool throw_on_block,
                                                        bool lockOnly) {
  RecMutexLock mutex_lock(dbc_conn_mutex, throw_on_block);
  bool valid = false;

  sql::Dbc_connection_handler::Ref myref(dbc_conn);
  if (dbc_conn && dbc_conn->ref.get_ptr()) {
    if (lockOnly) // this is a special case, we need it in some situations like for example recordset_cdbc
      return mutex_lock;

    try {
      // use connector::isValid to check if server connection is valid
      // this will also ping the server and reconnect if needed
      valid = dbc_conn->ref->isValid();
    } catch (std::exception &exc) {
      logError("CppConn::isValid exception: %s", exc.what());
      valid = false;
    }
    if (!valid) {
      bool user_connection = _usr_dbc_conn ? dbc_conn->ref.get_ptr() == _usr_dbc_conn->ref.get_ptr() : false;

      if (dbc_conn->autocommit_mode) {
        sql::AuthenticationSet authset;
        std::shared_ptr<SSHTunnel> tunnel = sql::DriverManager::getDriverManager()->getTunnel(_connection);

        create_connection(dbc_conn, _connection, tunnel, sql::Authentication::Ref(), dbc_conn->autocommit_mode,
                          user_connection);
        if (!dbc_conn->ref->isClosed())
          valid = true;
      }
    } else
      valid = true;
  }
  if (!valid)
    throw grt::db_not_connected("DBMS connection is not available");

  return mutex_lock;
}

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

bool SqlEditorForm::auto_commit() {
  if (_usr_dbc_conn)
    return _usr_dbc_conn->autocommit_mode;
  return false;
}

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

void SqlEditorForm::auto_commit(bool value) {
  if (!_usr_dbc_conn)
    return;
  {
    const char *STATEMENT = value ? "AUTOCOMMIT=1" : "AUTOCOMMIT=0";
    try {
      RecMutexLock usr_dbc_conn_mutex = ensure_valid_usr_connection();
      _usr_dbc_conn->ref->setAutoCommit(value);
      _usr_dbc_conn->autocommit_mode = _usr_dbc_conn->ref->getAutoCommit();
    }
    CATCH_ANY_EXCEPTION_AND_DISPATCH(STATEMENT)
  }
  update_menu_and_toolbar();
}

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

void SqlEditorForm::toggle_autocommit() {
  auto_commit(!auto_commit());
  update_menu_and_toolbar();
}

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

void SqlEditorForm::toggle_collect_field_info() {
  if (_connection.is_valid())
    _connection->parameterValues().set("CollectFieldMetadata", grt::IntegerRef(collect_field_info() ? 0 : 1));
  update_menu_and_toolbar();
}

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

bool SqlEditorForm::collect_field_info() const {
  if (_connection.is_valid())
    return _connection->parameterValues().get_int("CollectFieldMetadata", 1) != 0;
  return false;
}

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

void SqlEditorForm::toggle_collect_ps_statement_events() {
  if (_connection.is_valid())
    _connection->parameterValues().set("CollectPerfSchemaStatsForQueries",
                                       grt::IntegerRef(collect_ps_statement_events() ? 0 : 1));
  update_menu_and_toolbar();
}

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

bool SqlEditorForm::collect_ps_statement_events() const {
  if (_connection.is_valid() && is_supported_mysql_version_at_least(rdbms_version(), 5, 6))
    return _connection->parameterValues().get_int("CollectPerfSchemaStatsForQueries", 1) != 0;
  return false;
}

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

void SqlEditorForm::cancel_query() {
  std::string query_kill_query;
  {
    db_mgmt_RdbmsRef rdbms = db_mgmt_RdbmsRef::cast_from(_connection->driver()->owner());
    SqlFacade::Ref sql_facade = SqlFacade::instance_for_rdbms(rdbms);
    Sql_specifics::Ref sql_specifics = sql_facade->sqlSpecifics();
    query_kill_query = sql_specifics->query_kill_query(_usr_dbc_conn->id);
  }
  if (query_kill_query.empty())
    return;

  const char *STATEMENT = "INTERRUPT";
  RowId log_message_index = add_log_message(DbSqlEditorLog::BusyMsg, _("Running..."), STATEMENT, "");
  Timer timer(false);

  try {
    {
      RecMutexLock aux_dbc_conn_mutex(ensure_valid_aux_connection());
      std::unique_ptr<sql::Statement> stmt(_aux_dbc_conn->ref->createStatement());
      {
        base::ScopeExitTrigger schedule_timer_stop(std::bind(&Timer::stop, &timer));
        timer.run();
        stmt->execute(query_kill_query);

        // this can potentially cause threading issues, since connector driver isn't thread-safe
        // close_connection(_usr_dbc_conn);

        // connection drop doesn't interrupt fetching stage (surprisingly)
        // to workaround that we set special flag and check it periodically during fetching
        _usr_dbc_conn->is_stop_query_requested = is_running_query();
      }
    }

    if (_usr_dbc_conn->is_stop_query_requested) {
      bec::GRTManager::get()->replace_status_text("Query Cancelled");
      set_log_message(log_message_index, DbSqlEditorLog::NoteMsg, _("OK - Query cancelled"), STATEMENT,
                      timer.duration_formatted());
    } else
      set_log_message(log_message_index, DbSqlEditorLog::NoteMsg, _("OK - Query already completed"), STATEMENT,
                      timer.duration_formatted());

    // reconnect but only if in autocommit mode
    if (_usr_dbc_conn->autocommit_mode) {
      // this will restore connection if it was established previously
      exec_sql_task->execute_in_main_thread(std::bind(&SqlEditorForm::send_message_keep_alive, this), false, true);
    }
  }
  CATCH_SQL_EXCEPTION_AND_DISPATCH(STATEMENT, log_message_index, "")
}

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

void SqlEditorForm::commit() {
  exec_sql_retaining_editor_contents("COMMIT", nullptr, false);
}

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

void SqlEditorForm::rollback() {
  exec_sql_retaining_editor_contents("ROLLBACK", nullptr, false);
}

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

void SqlEditorForm::explain_current_statement() {
  SqlEditorPanel *panel = active_sql_editor_panel();
  if (panel) {
    SqlEditorResult *result = panel->add_panel_for_recordset(Recordset::Ref());
    result->set_title("Explain");

    grt::BaseListRef args(true);
    args.ginsert(panel->grtobj());
    args.ginsert(result->grtobj());
    // run the visual explain plugin, so it will fill the result panel
    grt::GRT::get()->call_module_function("SQLIDEQueryAnalysis", "visualExplain", args);
  }
}

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

// Should actually be called _retaining_old_recordsets
void SqlEditorForm::exec_sql_retaining_editor_contents(const std::string &sql_script, SqlEditorPanel *editor, bool sync,
                                                       bool dont_add_limit_clause) {
  auto_save();

  if (!connected())
    throw grt::db_not_connected("Not connected");

  if (editor) {
    editor->query_started(true);
    exec_sql_task->finish_cb(std::bind(&SqlEditorPanel::query_finished, editor), true);
    exec_sql_task->fail_cb(std::bind(&SqlEditorPanel::query_failed, editor, std::placeholders::_1), true);
  }

  exec_sql_task->exec(sync, std::bind(&SqlEditorForm::do_exec_sql, this, weak_ptr_from(this),
                                      std::shared_ptr<std::string>(new std::string(sql_script)), editor,
                                      (ExecFlags)(dont_add_limit_clause ? DontAddLimitClause : 0), RecordsetsRef()));
}

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

void SqlEditorForm::run_editor_contents(bool current_statement_only) {
  SqlEditorPanel *panel(active_sql_editor_panel());
  if (panel) {
    exec_editor_sql(panel, false, current_statement_only, current_statement_only);
  }
}

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

RecordsetsRef SqlEditorForm::exec_sql_returning_results(const std::string &sql_script, bool dont_add_limit_clause) {
  if (!connected())
    throw grt::db_not_connected("Not connected");

  RecordsetsRef rsets(new Recordsets());

  do_exec_sql(weak_ptr_from(this), std::shared_ptr<std::string>(new std::string(sql_script)), nullptr,
              (ExecFlags)(dont_add_limit_clause ? DontAddLimitClause : 0), rsets);

  return rsets;
}

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

/**
 * Runs the current content of the given editor on the target server and returns true if the query
 * was actually started (useful for the platform layers to show a busy animation).
 *
 * @param editor The editor whose content is to be executed.
 * @param sync If true wait for completion.
 * @param current_statement_only If true then only the statement where the cursor is in is executed.
 *                               Otherwise the current selection is executed (if there is one) or
 *                               the entire editor content.
 * @param use_non_std_delimiter If true the code is wrapped with a non standard delimiter to
 *                                    allow running the sql regardless of the delimiters used by the
 *                                    user (e.g. for view/sp definitions).
 * @param dont_add_limit_clause If true the automatic addition of the LIMIT clause is suppressed, which
 *                              is used to limit on the number of return rows (avoid huge result sets
 *                              by accident).
 * @param into_result If not nullptr, the resultset grid will be displayed inside it, instead of creating
 *                     a new one in editor. The query/script must return at most one recordset.
 */
bool SqlEditorForm::exec_editor_sql(SqlEditorPanel *editor, bool sync, bool current_statement_only,
                                    bool use_non_std_delimiter, bool dont_add_limit_clause,
                                    SqlEditorResult *into_result) {

  logDebug("Executing SQL in editor: %s (current statement only: %s)...\n", editor->get_title().c_str(),
           current_statement_only ? "yes" : "no");

  std::shared_ptr<std::string> shared_sql;
  if (current_statement_only)
    shared_sql.reset(new std::string(editor->editor_be()->current_statement()));
  else {
    std::string sql = editor->editor_be()->selected_text();
    if (sql.empty()) {
      std::pair<const char *, std::size_t> text = editor->text_data();
      shared_sql.reset(new std::string(text.first, text.second));
    } else
      shared_sql.reset(new std::string(sql));
  }

  if (shared_sql->empty())
    return false;

  ExecFlags flags = (ExecFlags)0;

  if (use_non_std_delimiter)
    flags = (ExecFlags)(flags | NeedNonStdDelimiter);
  if (dont_add_limit_clause)
    flags = (ExecFlags)(flags | DontAddLimitClause);
  if (bec::GRTManager::get()->get_app_option_int("DbSqlEditor:ShowWarnings", 1))
    flags = (ExecFlags)(flags | ShowWarnings);
  auto_save();

  // If we're filling an already existing result panel, we shouldn't close the old result sets.
  editor->query_started(into_result ? true : false);
  exec_sql_task->finish_cb(std::bind(&SqlEditorPanel::query_finished, editor), true);
  exec_sql_task->fail_cb(std::bind(&SqlEditorPanel::query_failed, editor, std::placeholders::_1), true);

  if (into_result) {
    logDebug2("Running into existing rsets\n");

    RecordsetsRef rsets(new Recordsets());

    exec_sql_task->exec(sync, std::bind(&SqlEditorForm::do_exec_sql, this, weak_ptr_from(this), shared_sql,
                                        (SqlEditorPanel *)nullptr, flags, rsets));

    if (rsets->size() > 1)
      logError("Statement returns too many resultsets\n");
    if (!rsets->empty())
      into_result->set_recordset((*rsets)[0]);
  } else {
    logDebug2("Running without considering existing rsets\n");

    exec_sql_task->exec(sync, std::bind(&SqlEditorForm::do_exec_sql, this, weak_ptr_from(this), shared_sql, editor,
                                        flags, RecordsetsRef()));
  }

  return true;
}

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

void SqlEditorForm::update_live_schema_tree(const std::string &sql) {
  bec::GRTManager::get()->run_once_when_idle(this, std::bind(&SqlEditorForm::handle_command_side_effects, this, sql));
}

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

grt::StringRef SqlEditorForm::do_exec_sql(Ptr self_ptr, std::shared_ptr<std::string> sql, SqlEditorPanel *editor,
                                          ExecFlags flags, RecordsetsRef result_list) {

  logDebug("Background task for sql execution started\n");

  bool use_non_std_delimiter = (flags & NeedNonStdDelimiter) != 0;
  bool dont_add_limit_clause = (flags & DontAddLimitClause) != 0;
  std::map<std::string, std::int64_t> ps_stats;
  std::vector<PSStage> ps_stages;
  std::vector<PSWait> ps_waits;
  bool query_ps_stats = collect_ps_statement_events();
  std::string query_ps_statement_events_error;
  std::string statement;
  int max_query_size_to_log = (int)bec::GRTManager::get()->get_app_option_int("DbSqlEditor:MaxQuerySizeToHistory", 0);
  int limit_rows = 0;
  if (bec::GRTManager::get()->get_app_option_int("SqlEditor:LimitRows") != 0)
    limit_rows = (int)bec::GRTManager::get()->get_app_option_int("SqlEditor:LimitRowsCount", 0);

  bec::GRTManager::get()->replace_status_text(_("Executing Query..."));

  std::shared_ptr<SqlEditorForm> self_ref = (self_ptr).lock();
  SqlEditorForm *self = (self_ref).get();
  if (!self) {
    logError("Couldn't aquire lock for SQL editor form\n");
    return grt::StringRef("");
  }

  // add_log_message() will increment this variable on errors or warnings
  _exec_sql_error_count = 0;

  bool interrupted = true;
  sql::Driver *dbc_driver = nullptr;
  try {
    RecMutexLock use_dbc_conn_mutex(ensure_valid_usr_connection());

    dbc_driver = _usr_dbc_conn->ref->getDriver();
    dbc_driver->threadInit();

    bool is_running_query = true;
    AutoSwap<bool> is_running_query_keeper(_is_running_query, is_running_query);
    update_menu_and_toolbar();

    _has_pending_log_messages = false;
    base::ScopeExitTrigger schedule_log_messages_refresh(std::bind(&SqlEditorForm::refresh_log_messages, this, true));

    SqlFacade::Ref sql_facade = SqlFacade::instance_for_rdbms(rdbms());
    Sql_syntax_check::Ref sql_syntax_check = sql_facade->sqlSyntaxCheck();
    Sql_specifics::Ref sql_specifics = sql_facade->sqlSpecifics();

    bool ran_set_sql_mode = false;
    bool logging_queries;
    std::vector<std::pair<std::size_t, std::size_t>> statement_ranges;
    sql_facade->splitSqlScript(sql->c_str(), sql->size(),
                               use_non_std_delimiter ? sql_specifics->non_std_sql_delimiter() : ";", statement_ranges);

    if (statement_ranges.size() > 1) {
      query_ps_stats = false;
      query_ps_statement_events_error = "Query stats can only be fetched when a single statement is executed.";
    }

    if (!max_query_size_to_log || max_query_size_to_log >= (int)sql->size()) {
      logging_queries = true;
    } else {
      std::list<std::string> warning;

      warning.push_back(base::strfmt("Skipping history entries for %li statements, total %li bytes",
                                     (long)statement_ranges.size(), (long)sql->size()));
      _history->add_entry(warning);
      logging_queries = false;
    }

    // Intentionally allow any value. For values <= 0 show no result set at all.
    ssize_t max_resultset_count = bec::GRTManager::get()->get_app_option_int("DbSqlEditor::MaxResultsets", 50);
    ssize_t total_result_count = (editor != nullptr) ? editor->resultset_count() : 0; // Consider pinned result sets.

    bool results_left = false;
    for (auto &statement_range : statement_ranges) {
      logDebug3("Executing statement range: %lu, %lu...\n", statement_range.first, statement_range.second);

      statement = sql->substr(statement_range.first, statement_range.second);
      std::list<std::string> sub_statements;
      sql_facade->splitSqlScript(statement, sub_statements);
      std::size_t multiple_statement_count = sub_statements.size();
      bool is_multiple_statement = (1 < multiple_statement_count);

      {
        statement = strip_text(statement, false, true);
        if (statement.empty())
          continue;

        Sql_syntax_check::Statement_type statement_type = sql_syntax_check->determine_statement_type(statement);

        logDebug3("Determined statement type: %u\n", statement_type);
        if (Sql_syntax_check::sql_empty == statement_type)
          continue;

        std::string schema_name;
        std::string table_name;

        if (logging_queries) {
          std::list<std::string> statements;
          statements.push_back(statement);
          _history->add_entry(statements);
        }

        Recordset_cdbc_storage::Ref data_storage;

        // for select queries add limit clause if specified by global option
        if (!is_multiple_statement && (Sql_syntax_check::sql_select == statement_type)) {
          data_storage = Recordset_cdbc_storage::create();
          data_storage->set_gather_field_info(true);
          data_storage->rdbms(rdbms());
          data_storage->setUserConnectionGetter(
            std::bind(&SqlEditorForm::getUserConnection, this, std::placeholders::_1, std::placeholders::_2));
          data_storage->setAuxConnectionGetter(
            std::bind(&SqlEditorForm::getAuxConnection, this, std::placeholders::_1, std::placeholders::_2));

          SqlFacade::String_tuple_list column_names;

          if (!table_name.empty() ||
              sql_facade->parseSelectStatementForEdit(statement, schema_name, table_name, column_names)) {
            data_storage->schema_name(schema_name.empty() ? _usr_dbc_conn->active_schema : schema_name);
            data_storage->table_name(table_name);
            logDebug3("Result will be editable\n");
          } else {
            data_storage->readonly_reason(
              "Statement must be a SELECT for columns of a single table with a primary key for its results to be "
              "editable.");
            logDebug3("Result will not be editable\n");
          }

          data_storage->sql_query(statement);

          {
            bool do_limit = !dont_add_limit_clause && limit_rows > 0;
            data_storage->limit_rows(do_limit);

            if (limit_rows > 0)
              data_storage->limit_rows_count(limit_rows);
          }
          statement = data_storage->decorated_sql_query();
        }

        {
          RowId log_message_index = add_log_message(DbSqlEditorLog::BusyMsg, _("Running..."), statement,
                                                    ((Sql_syntax_check::sql_select == statement_type) ? "? / ?" : "?"));

          bool statement_failed = false;
          long long updated_rows_count = -1;
          Timer statement_exec_timer(false);
          Timer statement_fetch_timer(false);
          std::shared_ptr<sql::Statement> dbc_statement(_usr_dbc_conn->ref->createStatement());
          bool is_result_set_first = false;

          if (_usr_dbc_conn->is_stop_query_requested)
            throw std::runtime_error(
              _("Query execution has been stopped, the connection to the DB server was not restarted, any open "
                "transaction remains open"));

          try {
            {
              base::ScopeExitTrigger schedule_statement_exec_timer_stop(std::bind(&Timer::stop, &statement_exec_timer));
              statement_exec_timer.run();
              is_result_set_first = dbc_statement->execute(statement);
            }
            logDebug3("Query executed successfully\n");

            updated_rows_count = dbc_statement->getUpdateCount();

            // XXX: coalesce all the special queries here and act on them *after* all queries have run.
            // Especially the drop command is redirected twice to idle tasks, kicking so in totally asynchronously
            // and killing any intermittent USE commands.
            // Updating the UI during a run of many commands is not useful either.
            if (Sql_syntax_check::sql_use == statement_type)
              cache_active_schema_name();
            if (Sql_syntax_check::sql_set == statement_type && statement.find("@sql_mode") != std::string::npos)
              ran_set_sql_mode = true;
            if (Sql_syntax_check::sql_drop == statement_type)
              update_live_schema_tree(statement);
          } catch (sql::SQLException &e) {
            std::string err_msg;
            // safe mode
            switch (e.getErrorCode()) {
              case 1046: // not default DB selected
                err_msg = strfmt(_("Error Code: %i. %s\nSelect the default DB to be used by double-clicking its name "
                                   "in the SCHEMAS list in the sidebar."),
                                 e.getErrorCode(), e.what());
                break;
              case 1175: // safe mode
                err_msg = strfmt(_("Error Code: %i. %s\nTo disable safe mode, toggle the option in Preferences -> SQL "
                                   "Editor and reconnect."),
                                 e.getErrorCode(), e.what());
                break;
              default:
                err_msg = strfmt(_("Error Code: %i. %s"), e.getErrorCode(), e.what());
                break;
            }
            set_log_message(log_message_index, DbSqlEditorLog::ErrorMsg, err_msg, statement,
                            statement_exec_timer.duration_formatted());
            statement_failed = true;
          } catch (std::exception &e) {
            std::string err_msg = strfmt(_("Error: %s"), e.what());
            set_log_message(log_message_index, DbSqlEditorLog::ErrorMsg, err_msg, statement,
                            statement_exec_timer.duration_formatted());
            statement_failed = true;
          }
          if (statement_failed) {
            if (_continueOnError)
              continue; // goto next statement
            else
              goto stop_processing_sql_script;
          }

          sql::mysql::MySQL_Connection *mysql_connection =
            dynamic_cast<sql::mysql::MySQL_Connection *>(dbc_statement->getConnection());
          sql::SQLString last_statement_info;
          if (mysql_connection != nullptr)
            last_statement_info = mysql_connection->getLastStatementInfo();
          if (updated_rows_count >= 0) {
            std::string message = strfmt(_("%lli row(s) affected"), updated_rows_count);
            bool has_warning = false;
            if (flags & ShowWarnings) {
              std::string warnings_message;
              const sql::SQLWarning *warnings = dbc_statement->getWarnings();
              if (warnings) {
                int count = 0;
                const sql::SQLWarning *w = warnings;
                while (w) {
                  warnings_message.append(strfmt("\n%i %s", w->getErrorCode(), w->getMessage().c_str()));
                  count++;
                  w = w->getNextWarning();
                }
                message.append(strfmt(_(", %i warning(s):"), count));
                has_warning = true;
              }
              if (!warnings_message.empty())
                message.append(warnings_message);
            }
            if (!last_statement_info->empty())
              message.append("\n").append(last_statement_info);
            set_log_message(log_message_index, has_warning ? DbSqlEditorLog::WarningMsg : DbSqlEditorLog::OKMsg,
                            message, statement, statement_exec_timer.duration_formatted());
          }

          logDebug2("Processing result sets\n");
          int resultset_count = 0;
          bool more_results = is_result_set_first;
          bool reuse_log_msg = false;
          if ((updated_rows_count < 0) || is_multiple_statement) {
            for (std::size_t processed_substatements_count = 0;
                 processed_substatements_count < multiple_statement_count; ++processed_substatements_count) {
              do {
                if (more_results) {
                  if (total_result_count == max_resultset_count) {
                    int result = mforms::Utilities::show_warning(
                      _("Maximum result count reached"),
                      "No further result tabs will be displayed as the maximm number has been reached. \nYou may stop "
                      "the operation, leaving the connection out of sync. You'll have to got o 'Query->Reconnect to "
                      "server' menu item to reset the state.\n\n Do you want to cancel the operation?",
                      "Yes", "No");
                    if (result == mforms::ResultOk) {
                      add_log_message(DbSqlEditorLog::ErrorMsg,
                                      "No more results could be displayed. Operation canceled by user.", statement,
                                      "");
                      dbc_statement->cancel();
                      dbc_statement->close();
                      return grt::StringRef("");
                    }
                    add_log_message(
                      DbSqlEditorLog::WarningMsg,
                      "No more results will be displayed because the maximum number of result sets was reached.",
                      statement, "");
                  }

                  if (!reuse_log_msg && ((updated_rows_count >= 0) || (resultset_count)))
                    log_message_index = add_log_message(DbSqlEditorLog::BusyMsg, _("Fetching..."), statement, "- / ?");
                  else
                    set_log_message(log_message_index, DbSqlEditorLog::BusyMsg, _("Fetching..."), statement,
                                    statement_exec_timer.duration_formatted() + " / ?");
                  reuse_log_msg = false;
                  std::shared_ptr<sql::ResultSet> dbc_resultset;
                  {
                    base::ScopeExitTrigger schedule_statement_fetch_timer_stop(
                      std::bind(&Timer::stop, &statement_fetch_timer));
                    statement_fetch_timer.run();

                    // need a separate exception catcher here, because sometimes a query error
                    // will only throw an exception after fetching starts, which causes the busy spinner
                    // to be active forever, since the exception is logged in a new log_id/row
                    // XXX this could also be caused by a bug in Connector/C++
                    try {
                      dbc_resultset.reset(dbc_statement->getResultSet());
                    } catch (sql::SQLException &e) {
                      std::string err_msg;
                      // safe mode
                      switch (e.getErrorCode()) {
                        case 1046: // not default DB selected
                          err_msg = strfmt(_("Error Code: %i. %s\nSelect the default DB to be used by double-clicking "
                                             "its name in the SCHEMAS list in the sidebar."),
                                           e.getErrorCode(), e.what());
                          break;
                        case 1175: // safe mode
                          err_msg = strfmt(_("Error Code: %i. %s\nTo disable safe mode, toggle the option in "
                                             "Preferences -> SQL Editor and reconnect."),
                                           e.getErrorCode(), e.what());
                          break;
                        default:
                          err_msg = strfmt(_("Error Code: %i. %s"), e.getErrorCode(), e.what());
                          break;
                      }

                      set_log_message(log_message_index, DbSqlEditorLog::ErrorMsg, err_msg, statement,
                                      statement_exec_timer.duration_formatted());

                      if (_continueOnError)
                        continue; // goto next statement
                      else
                        goto stop_processing_sql_script;
                    }
                  }

                  std::string exec_and_fetch_durations =
                    (((updated_rows_count >= 0) || (resultset_count)) ? std::string("-")
                                                                      : statement_exec_timer.duration_formatted()) +
                    " / " + statement_fetch_timer.duration_formatted();
                  if (total_result_count >= max_resultset_count)
                    set_log_message(log_message_index, DbSqlEditorLog::OKMsg, "Row count could not be verified",
                                    statement, exec_and_fetch_durations);
                  else if (dbc_resultset) {
                    if (!data_storage) {
                      data_storage = Recordset_cdbc_storage::create();
                      data_storage->set_gather_field_info(true);
                      data_storage->rdbms(rdbms());
                      data_storage->setUserConnectionGetter(std::bind(&SqlEditorForm::getUserConnection, this,
                                                                      std::placeholders::_1, std::placeholders::_2));
                      data_storage->setAuxConnectionGetter(std::bind(&SqlEditorForm::getAuxConnection, this,
                                                                     std::placeholders::_1, std::placeholders::_2));
                      if (table_name.empty())
                        data_storage->sql_query(statement);
                      data_storage->schema_name(schema_name);
                      data_storage->table_name(table_name);
                    }

                    data_storage->dbc_statement(dbc_statement);
                    data_storage->dbc_resultset(dbc_resultset);
                    data_storage->reloadable(!is_multiple_statement &&
                                             (Sql_syntax_check::sql_select == statement_type));

                    logDebug3("Creation and setup of a new result set...\n");

                    Recordset::Ref rs = Recordset::create(exec_sql_task);
                    rs->is_field_value_truncation_enabled(true);
                    rs->setPreserveRowFilter(
                      bec::GRTManager::get()->get_app_option_int("SqlEditor:PreserveRowFilter") == 1);
                    rs->apply_changes_cb =
                      std::bind(&SqlEditorForm::apply_changes_to_recordset, this, Recordset::Ptr(rs));
                    rs->generator_query(statement);

                    {
                      if (query_ps_stats) {
                        query_ps_statistics(_usr_dbc_conn->id, ps_stats);
                        ps_stages = query_ps_stages(ps_stats["EVENT_ID"]);
                        ps_waits = query_ps_waits(ps_stats["EVENT_ID"]);
                        query_ps_stats = false;
                      }
                      RecordsetData *rdata = new RecordsetData();
                      rdata->duration = statement_exec_timer.duration();
                      rdata->ps_stat_error = query_ps_statement_events_error;
                      rdata->ps_stat_info = ps_stats;
                      rdata->ps_stage_info = ps_stages;
                      rdata->ps_wait_info = ps_waits;
                      rs->set_client_data(rdata);
                    }

                    rs->data_storage(data_storage);
                    rs->reset(true);

                    if (data_storage->valid()) // query statement
                    {
                      if (result_list)
                        result_list->push_back(rs);

                      if (editor)
                        editor->add_panel_for_recordset_from_main(rs);

                      std::string statement_res_msg = std::to_string(rs->row_count()) + _(" row(s) returned");
                      if (!last_statement_info->empty())
                        statement_res_msg.append("\n").append(last_statement_info);

                      set_log_message(log_message_index, DbSqlEditorLog::OKMsg, statement_res_msg, statement,
                                      exec_and_fetch_durations);
                    }
                    ++resultset_count;
                  } else {
                    reuse_log_msg = true;
                  }
                  ++total_result_count;
                  data_storage.reset();
                }
              } while ((more_results = dbc_statement->getMoreResults()));
            }
          }

          if ((updated_rows_count < 0) && !(resultset_count))
            set_log_message(log_message_index, DbSqlEditorLog::OKMsg, _("OK"), statement,
                            statement_exec_timer.duration_formatted());
        }
      }
    } // statement range loop

    if (results_left) {
      exec_sql_task->execute_in_main_thread(
        std::bind(&mforms::Utilities::show_warning, _("Result set limit reached"),
                  _("There were more results than "
                    "result tabs could be opened, because the set maximum limit was reached. You can change this "
                    "limit in the preferences."),
                  _("OK"), "", ""),
        true, false);
    }

    bec::GRTManager::get()->replace_status_text(_("Query Completed"));
    interrupted = false;

  stop_processing_sql_script:
    if (interrupted)
      bec::GRTManager::get()->replace_status_text(_("Query interrupted"));
    // try to minimize the times this is called, since this will change the state of the connection
    // after a user query is ran (eg, it will reset all warnings)
    if (ran_set_sql_mode)
      cache_sql_mode();
  }
  CATCH_ANY_EXCEPTION_AND_DISPATCH(statement)

  if (dbc_driver)
    dbc_driver->threadEnd();

  logDebug("SQL execution finished\n");

  update_menu_and_toolbar();

  _usr_dbc_conn->is_stop_query_requested = false;

  return grt::StringRef("");
}

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

void SqlEditorForm::exec_management_sql(const std::string &sql, bool log) {
  sql::Dbc_connection_handler::Ref conn;
  base::RecMutexLock lock(ensure_valid_aux_connection(conn));
  if (conn) {
    RowId rid = log ? add_log_message(DbSqlEditorLog::BusyMsg, _("Executing "), sql, "- / ?") : 0;
    const std::unique_ptr<sql::Statement> stmt(conn->ref->createStatement());
    Timer statement_exec_timer(false);
    try {
      stmt->execute(sql);
    } catch (sql::SQLException &e) {
      if (log)
        set_log_message(rid, DbSqlEditorLog::ErrorMsg, strfmt(SQL_EXCEPTION_MSG_FORMAT, e.getErrorCode(), e.what()),
                        sql, "");
      throw;
    }
    CATCH_EXCEPTION_AND_DISPATCH(sql);

    if (log)
      set_log_message(rid, DbSqlEditorLog::OKMsg, _("OK"), sql, statement_exec_timer.duration_formatted());

    handle_command_side_effects(sql);
  }
}

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

void SqlEditorForm::exec_main_sql(const std::string &sql, bool log) {
  base::RecMutexLock lock(ensure_valid_usr_connection());
  if (_usr_dbc_conn) {
    RowId rid = log ? add_log_message(DbSqlEditorLog::BusyMsg, _("Executing "), sql, "- / ?") : 0;
    const std::unique_ptr<sql::Statement> stmt(_usr_dbc_conn->ref->createStatement());
    Timer statement_exec_timer(false);
    try {
      stmt->execute(sql);
    } catch (sql::SQLException &e) {
      if (log)
        set_log_message(rid, DbSqlEditorLog::ErrorMsg, strfmt(SQL_EXCEPTION_MSG_FORMAT, e.getErrorCode(), e.what()),
                        sql, "");
      throw;
    }
    CATCH_EXCEPTION_AND_DISPATCH(sql);

    if (log)
      set_log_message(rid, DbSqlEditorLog::OKMsg, _("OK"), sql, statement_exec_timer.duration_formatted());

    handle_command_side_effects(sql);
  }
}

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

static wb::LiveSchemaTree::ObjectType str_to_object_type(const std::string &object_type) {
  if (object_type == "db.Table")
    return LiveSchemaTree::Table;
  else if (object_type == "db.View")
    return LiveSchemaTree::View;
  else if (object_type == "db.StoredProcedure")
    return LiveSchemaTree::Procedure;
  else if (object_type == "db.Function")
    return LiveSchemaTree::Function;
  else if (object_type == "db.Index")
    return LiveSchemaTree::Index;
  else if (object_type == "db.Trigger")
    return LiveSchemaTree::Trigger;
  else if (object_type == "db.Schema")
    return LiveSchemaTree::Schema;

  return LiveSchemaTree::NoneType;
}

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

void SqlEditorForm::handle_command_side_effects(const std::string &sql) {
  SqlFacade::Ref sql_facade = SqlFacade::instance_for_rdbms(rdbms());

  std::string object_type;
  std::string schema_name = active_schema();
  std::vector<std::pair<std::string, std::string>> object_names;

  // special hack, check for some special commands and update UI accordingly
  if (sql_facade->parseDropStatement(sql, object_type, object_names) && !object_names.empty()) {
    wb::LiveSchemaTree::ObjectType obj = str_to_object_type(object_type);
    if (obj != wb::LiveSchemaTree::NoneType) {
      std::vector<std::pair<std::string, std::string>>::reverse_iterator rit;

      if (obj == wb::LiveSchemaTree::Schema) {
        for (rit = object_names.rbegin(); rit != object_names.rend(); ++rit)
          _live_tree->refresh_live_object_in_overview(obj, (*rit).first, (*rit).first, "");

        if (!object_names.empty())
          schema_name = object_names.back().first;

        if ((schema_name.size() > 0) && (active_schema() == schema_name) && connection_descriptor().is_valid()) {
          std::string default_schema = connection_descriptor()->parameterValues().get_string("schema", "");
          if (schema_name == default_schema)
            default_schema = "";
          bec::GRTManager::get()->run_once_when_idle(this,
                                                     std::bind(&set_active_schema, shared_from_this(), default_schema));
        }
      } else {
        for (rit = object_names.rbegin(); rit != object_names.rend(); ++rit)
          _live_tree->refresh_live_object_in_overview(obj, (*rit).first.empty() ? schema_name : (*rit).first,
                                                      (*rit).second, "");
      }
    }
  }
}

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

db_query_ResultsetRef SqlEditorForm::exec_management_query(const std::string &sql, bool log) {
  sql::Dbc_connection_handler::Ref conn;
  base::RecMutexLock lock(ensure_valid_aux_connection(conn));
  if (conn) {
    RowId rid = log ? add_log_message(DbSqlEditorLog::BusyMsg, _("Executing "), sql, "- / ?") : 0;
    const std::unique_ptr<sql::Statement> stmt(conn->ref->createStatement());
    Timer statement_exec_timer(false);
    try {
      std::shared_ptr<sql::ResultSet> results(stmt->executeQuery(sql));

      if (log)
        set_log_message(rid, DbSqlEditorLog::OKMsg, _("OK"), sql, statement_exec_timer.duration_formatted());

      return grtwrap_recordset(grtobj(), results);
    } catch (sql::SQLException &e) {
      if (log)
        set_log_message(rid, DbSqlEditorLog::ErrorMsg, strfmt(SQL_EXCEPTION_MSG_FORMAT, e.getErrorCode(), e.what()),
                        sql, "");
      throw;
    }
  }
  return db_query_ResultsetRef();
}

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

db_query_ResultsetRef SqlEditorForm::exec_main_query(const std::string &sql, bool log) {
  base::RecMutexLock lock(ensure_valid_usr_connection());
  if (_usr_dbc_conn) {
    RowId rid = log ? add_log_message(DbSqlEditorLog::BusyMsg, _("Executing "), sql, "- / ?") : 0;
    const std::unique_ptr<sql::Statement> stmt(_usr_dbc_conn->ref->createStatement());
    Timer statement_exec_timer(false);
    try {
      std::shared_ptr<sql::ResultSet> results(stmt->executeQuery(sql));

      if (log)
        set_log_message(rid, DbSqlEditorLog::OKMsg, _("OK"), sql, statement_exec_timer.duration_formatted());

      return grtwrap_recordset(grtobj(), results);
    } catch (sql::SQLException &e) {
      if (log)
        set_log_message(rid, DbSqlEditorLog::ErrorMsg, strfmt(SQL_EXCEPTION_MSG_FORMAT, e.getErrorCode(), e.what()),
                        sql, "");
      throw;
    }
  }
  return db_query_ResultsetRef();
}

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

bool SqlEditorForm::is_running_query() {
  return _is_running_query;
}

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

void SqlEditorForm::continue_on_error(bool val) {
  if (_continueOnError == val)
    return;

  _continueOnError = val;
  bec::GRTManager::get()->set_app_option("DbSqlEditor:ContinueOnError", grt::IntegerRef((int)_continueOnError));

  if (_menu)
    _menu->set_item_checked("query.continueOnError", continue_on_error());
  set_editor_tool_items_checked("query.continueOnError", continue_on_error());
}

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

void SqlEditorForm::send_message_keep_alive() {
  try {
    logDebug3("KeepAliveInterval tick\n");
    // ping server and reset connection timeout counter
    // this also checks the connection state and restores it if possible
    ensure_valid_aux_connection();
    ensure_valid_usr_connection();
  } catch (const std::exception &) {
  }
}

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

void SqlEditorForm::apply_changes_to_recordset(Recordset::Ptr rs_ptr) {
  RETURN_IF_FAIL_TO_RETAIN_WEAK_PTR(Recordset, rs_ptr, rs)

  try {
    bool auto_commit = false;

    // we need transaction to enforce atomicity of change set
    // so if autocommit is currently enabled disable it temporarily
    {
      RecMutexLock usr_dbc_conn_mutex = ensure_valid_usr_connection();
      auto_commit = _usr_dbc_conn->ref->getAutoCommit();
    }
    ScopeExitTrigger autocommit_mode_keeper;
    int res = -2;

    if (!auto_commit) {
      res = mforms::Utilities::show_warning(
        _("Apply Changes to Recordset"),
        _("Autocommit is currently disabled and a transaction might be open.\n"
          "Recordset changes will be applied within that transaction and will be left uncommited until you explicitly "
          "commit it manually.\n"
          "If you want it to be executed separately, click Cancel and commit the transaction first."),
        _("Apply"), _("Cancel"));
    } else {
      autocommit_mode_keeper.slot = std::bind(&sql::Connection::setAutoCommit, _usr_dbc_conn->ref.get(), auto_commit);
      RecMutexLock usr_dbc_conn_mutex = ensure_valid_usr_connection();
      _usr_dbc_conn->ref->setAutoCommit(false);
    }

    if (res != mforms::ResultCancel) // only if not canceled
    {
      on_sql_script_run_error.disconnect_all_slots();
      on_sql_script_run_progress.disconnect_all_slots();
      on_sql_script_run_statistics.disconnect_all_slots();

      Recordset_data_storage::Ref data_storage_ref = rs->data_storage();
      Recordset_sql_storage *sql_storage = dynamic_cast<Recordset_sql_storage *>(data_storage_ref.get());

      scoped_connection c1(
        on_sql_script_run_error.connect(std::bind(&SqlEditorForm::add_log_message, this, DbSqlEditorLog::ErrorMsg,
                                                  std::placeholders::_2, std::placeholders::_3, "")));

      bool skip_commit;
      if (auto_commit)
        skip_commit = false;
      else
        skip_commit = true; // if we're in an open tx, then do not commit

      bool is_data_changes_commit_wizard_enabled =
        (0 != bec::GRTManager::get()->get_app_option_int("DbSqlEditor:IsDataChangesCommitWizardEnabled", 1));
      if (is_data_changes_commit_wizard_enabled) {
        run_data_changes_commit_wizard(rs_ptr, skip_commit);
      } else {
        sql_storage->is_sql_script_substitute_enabled(false);

        scoped_connection on_sql_script_run_error_conn(
          sql_storage->on_sql_script_run_error.connect(on_sql_script_run_error));
        rs->do_apply_changes(rs_ptr, Recordset_data_storage::Ptr(data_storage_ref), skip_commit);
      }

      // Since many messages could have been added it is possible the
      // the action log has not been refresh, this triggers a refresh
      refresh_log_messages(true);
    }
  }
  CATCH_ANY_EXCEPTION_AND_DISPATCH(_("Apply changes to recordset"))
}

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

bool SqlEditorForm::run_data_changes_commit_wizard(Recordset::Ptr rs_ptr, bool skip_commit) {
  RETVAL_IF_FAIL_TO_RETAIN_WEAK_PTR(Recordset, rs_ptr, rs, false)

  // set underlying recordset data storage to use sql substitute (potentially modified by user)
  // instead of generating sql based on swap db contents
  Recordset_data_storage::Ref data_storage_ref = rs->data_storage();
  Recordset_sql_storage *sql_storage = dynamic_cast<Recordset_sql_storage *>(data_storage_ref.get());
  if (!sql_storage)
    return false;
  sql_storage->init_sql_script_substitute(rs_ptr, true);
  sql_storage->is_sql_script_substitute_enabled(true);
  const Sql_script &sql_script = sql_storage->sql_script_substitute();
  ;
  std::string sql_script_text = Recordset_sql_storage::statements_as_sql_script(sql_script.statements);

  // No need for online DDL settings or callback as we are dealing with data here, not metadata.
  SqlScriptRunWizard wizard(rdbms_version(), "", "");

  scoped_connection c1(
    on_sql_script_run_error.connect(std::bind(&SqlScriptApplyPage::on_error, wizard.apply_page, std::placeholders::_1,
                                              std::placeholders::_2, std::placeholders::_3)));
  scoped_connection c2(on_sql_script_run_progress.connect(
    std::bind(&SqlScriptApplyPage::on_exec_progress, wizard.apply_page, std::placeholders::_1)));
  scoped_connection c3(on_sql_script_run_statistics.connect(
    std::bind(&SqlScriptApplyPage::on_exec_stat, wizard.apply_page, std::placeholders::_1, std::placeholders::_2)));
  wizard.values().gset("sql_script", sql_script_text);
  wizard.apply_page->apply_sql_script =
    std::bind(&SqlEditorForm::apply_data_changes_commit, this, std::placeholders::_1, rs_ptr, skip_commit);
  wizard.run_modal();

  return !wizard.has_errors();
}

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

void SqlEditorForm::apply_object_alter_script(const std::string &alter_script, bec::DBObjectEditorBE *obj_editor,
                                              RowId log_id) {
  set_log_message(
    log_id, DbSqlEditorLog::BusyMsg, "",
    obj_editor ? strfmt(_("Applying changes to %s..."), obj_editor->get_name().c_str()) : _("Applying changes..."), "");

  SqlFacade::Ref sql_splitter = SqlFacade::instance_for_rdbms(rdbms());
  std::list<std::string> statements;
  sql_splitter->splitSqlScript(alter_script, statements);

  int max_query_size_to_log = (int)bec::GRTManager::get()->get_app_option_int("DbSqlEditor:MaxQuerySizeToHistory", 0);

  sql::SqlBatchExec sql_batch_exec;
  sql_batch_exec.stop_on_error(true);

  sql_batch_exec.error_cb(std::ref(on_sql_script_run_error));
  sql_batch_exec.batch_exec_progress_cb(std::ref(on_sql_script_run_progress));
  sql_batch_exec.batch_exec_stat_cb(std::ref(on_sql_script_run_statistics));

  long sql_batch_exec_err_count = 0;
  {
    try {
      RecMutexLock usr_dbc_conn_mutex(ensure_valid_usr_connection(true));
      std::unique_ptr<sql::Statement> stmt(_usr_dbc_conn->ref->createStatement());
      sql_batch_exec_err_count = sql_batch_exec(stmt.get(), statements);
    } catch (sql::SQLException &e) {
      set_log_message(log_id, DbSqlEditorLog::ErrorMsg, strfmt(SQL_EXCEPTION_MSG_FORMAT, e.getErrorCode(), e.what()),
                      strfmt(_("Apply ALTER script for %s"), obj_editor->get_name().c_str()), "");
      throw; // re-throw exception so that the wizard will see that something went wrong
    } catch (base::mutex_busy_error &) {
      set_log_message(log_id, DbSqlEditorLog::ErrorMsg,
                      strfmt(EXCEPTION_MSG_FORMAT, "Your connection to MySQL is currently busy. Please retry later."),
                      strfmt(_("Apply ALTER script for %s"), obj_editor->get_name().c_str()), "");
      throw std::runtime_error("Connection to MySQL currently busy.");
    } catch (std::exception &e) {
      set_log_message(log_id, DbSqlEditorLog::ErrorMsg, strfmt(EXCEPTION_MSG_FORMAT, e.what()),
                      strfmt(_("Apply ALTER script for %s"), obj_editor->get_name().c_str()), "");
      throw;
    }
  }

  if (!max_query_size_to_log || max_query_size_to_log >= (int)alter_script.size())
    _history->add_entry(sql_batch_exec.sql_log());

  // refresh object's state only on success, to not lose changes made by user
  if (obj_editor && (0 == sql_batch_exec_err_count)) {
    db_DatabaseObjectRef db_object = obj_editor->get_dbobject();

    set_log_message(log_id, DbSqlEditorLog::OKMsg, strfmt(_("Changes applied to %s"), obj_editor->get_name().c_str()),
                    "", "");

    // refresh state of created/altered object in physical overview
    {
      std::string schema_name = db_SchemaRef::can_wrap(db_object) ? std::string() : *db_object->owner()->name();
      db_SchemaRef schema;
      if (!schema_name.empty())
        schema = db_SchemaRef::cast_from(db_object->owner());

      wb::LiveSchemaTree::ObjectType db_object_type = wb::LiveSchemaTree::Any;
      if (db_SchemaRef::can_wrap(db_object))
        db_object_type = wb::LiveSchemaTree::Schema;
      else if (db_TableRef::can_wrap(db_object))
        db_object_type = wb::LiveSchemaTree::Table;
      else if (db_ViewRef::can_wrap(db_object))
        db_object_type = wb::LiveSchemaTree::View;
      else if (db_RoutineRef::can_wrap(db_object)) {
        db_RoutineRef db_routine = db_RoutineRef::cast_from(db_object);

        std::string obj_type = db_routine->routineType();

        if (obj_type == "function")
          db_object_type = wb::LiveSchemaTree::Function;
        else
          db_object_type = wb::LiveSchemaTree::Procedure;
      }

      //_live_tree->refresh_live_object_in_overview(db_object_type, schema_name, db_object->oldName(),
      //db_object->name());
      // Run refresh on main thread, but only if there's not another refresh pending already.
      if (!_overviewRefreshPending.connected()) {
        _overviewRefreshPending = bec::GRTManager::get()->run_once_when_idle(
          this, std::bind(&SqlEditorTreeController::refresh_live_object_in_overview, _live_tree, db_object_type,
                          schema_name, db_object->oldName(), db_object->name()));
      }
    }

    //_live_tree->refresh_live_object_in_editor(obj_editor, false);
    if (!_editorRefreshPending.connected()) {
      _editorRefreshPending = bec::GRTManager::get()->run_once_when_idle(
        this, std::bind(&SqlEditorTreeController::refresh_live_object_in_editor, _live_tree, obj_editor, false));
    }
  }
}

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

void SqlEditorForm::apply_data_changes_commit(const std::string &sql_script_text, Recordset::Ptr rs_ptr,
                                              bool skip_commit) {
  RETURN_IF_FAIL_TO_RETAIN_WEAK_PTR(Recordset, rs_ptr, rs);

  // this lock is supposed to be acquired lower in call-stack by SqlEditorForm::apply_changes_to_recordset
  // MutexLock usr_conn_mutex= ensure_valid_usr_connection();

  Recordset_data_storage::Ref data_storage_ref = rs->data_storage();
  Recordset_sql_storage *sql_storage = dynamic_cast<Recordset_sql_storage *>(data_storage_ref.get());
  if (!sql_storage)
    return;

  int max_query_size_to_log = (int)bec::GRTManager::get()->get_app_option_int("DbSqlEditor:MaxQuerySizeToHistory", 0);

  Sql_script sql_script = sql_storage->sql_script_substitute();
  sql_script.statements.clear();
  SqlFacade::Ref sql_splitter = SqlFacade::instance_for_rdbms(rdbms());
  sql_splitter->splitSqlScript(sql_script_text, sql_script.statements);

  scoped_connection on_sql_script_run_error_conn(sql_storage->on_sql_script_run_error.connect(on_sql_script_run_error));
  scoped_connection on_sql_script_run_progress_conn(
    sql_storage->on_sql_script_run_progress.connect(on_sql_script_run_progress));
  scoped_connection on_sql_script_run_statistics_conn(
    sql_storage->on_sql_script_run_statistics.connect(on_sql_script_run_statistics));

  sql_storage->sql_script_substitute(sql_script);
  rs->setPreserveRowFilter(bec::GRTManager::get()->get_app_option_int("SqlEditor:PreserveRowFilter") == 1);
  rs->do_apply_changes(rs_ptr, Recordset_data_storage::Ptr(data_storage_ref), skip_commit);

  if (!max_query_size_to_log || max_query_size_to_log >= (int)sql_script_text.size())
    _history->add_entry(sql_script.statements);
}

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

std::string SqlEditorForm::active_schema() const {
  return (_usr_dbc_conn) ? _usr_dbc_conn->active_schema : std::string();
}

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

void SqlEditorForm::schemaListRefreshed(std::vector<std::string> const &schemas) {
  std::unique_lock<std::mutex> lock(_pimplMutex->_symbolsMutex);
  _databaseSymbols.clear(); // Doesn't clear the dependencies.

  for (auto schema : schemas) {
    _databaseSymbols.addNewSymbol<SchemaSymbol>(nullptr, schema);
  }
}

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

/**
 * Reads all relevant built-in symbols like engines and collations in our static server symbols list.
 */
void SqlEditorForm::readStaticServerSymbols() {
  std::unique_lock<std::mutex> lock(_pimplMutex->_symbolsMutex); // Probably not needed, as this runs during startup.

  if (_usr_dbc_conn->ref.get() != nullptr) {
    const std::unique_ptr<sql::Statement> statement(_usr_dbc_conn->ref.get()->createStatement());

    {
      const std::unique_ptr<sql::ResultSet> rs(statement->executeQuery("show engines"));
      while (rs->next()) {
        std::string name = rs->getString(1);
        std::string support = rs->getString(2);
        if (support != "NO") { // Can be YES, NO or DEFAULT.
          _staticServerSymbols.addNewSymbol<EngineSymbol>(nullptr, name);
        }
      }
    }

    {
      const std::unique_ptr<sql::ResultSet> rs(statement->executeQuery("show charset"));
      while (rs->next()) {
        _staticServerSymbols.addNewSymbol<CharsetSymbol>(nullptr, rs->getString(1));
      }
    }

    {
      const std::unique_ptr<sql::ResultSet> rs(statement->executeQuery("show collation"));
      while (rs->next()) {
        _staticServerSymbols.addNewSymbol<CollationSymbol>(nullptr, rs->getString(1));
      }
    }

    {
      const std::unique_ptr<sql::ResultSet> rs(statement->executeQuery("show variables"));
      while (rs->next()) {
        _staticServerSymbols.addNewSymbol<SystemVariableSymbol>(nullptr, "@@" + rs->getString(1));
      }
    }

  }
}

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

/**
 * Notification from the tree controller that (some) schema meta data has been refreshed. We use this
 * info to update the database symbol table.
 */
void SqlEditorForm::schema_meta_data_refreshed(const std::string &schema_name, base::StringListPtr tables,
                                               base::StringListPtr views, base::StringListPtr procedures,
                                               base::StringListPtr functions) {
  std::unique_lock<std::mutex> lock(_pimplMutex->_symbolsMutex);
  std::unique_ptr<sql::Statement> statement;
  RecMutexLock usr_dbc_conn_mutex(ensure_valid_usr_connection());
  if (_usr_dbc_conn->ref.get() != nullptr)
    statement.reset(_usr_dbc_conn->ref->createStatement());

  auto schemaSymbols = _databaseSymbols.getSymbolsOfType<SchemaSymbol>();
  bool hasPerformanceSchema = std::find_if(schemaSymbols.begin(), schemaSymbols.end(), [](auto symbol) -> bool {
    return symbol->name == "performance_schema";
  }) != schemaSymbols.end();

  for (SchemaSymbol *schemaSymbol : schemaSymbols) {
    if (schemaSymbol->name == schema_name) {
      schemaSymbol->clear();
      for (auto table : *tables) {
        TableSymbol *tableSymbol = _databaseSymbols.addNewSymbol<TableSymbol>(schemaSymbol, table);

        // Fetch column info for each table.
        if (statement != nullptr) {
          std::unique_ptr<sql::ResultSet> rs(statement->executeQuery(
            std::string(base::sqlstring("SHOW FULL COLUMNS FROM !.!", 0) << schema_name << table)));

          while (rs->next()) {
            _databaseSymbols.addNewSymbol<ColumnSymbol>(tableSymbol, rs->getString(1), nullptr);
          }
        }
      }

      for (auto view : *views) {
        ViewSymbol *viewSymbol = _databaseSymbols.addNewSymbol<ViewSymbol>(schemaSymbol, view);

        // Same for each view.
        if (statement != nullptr) {
          std::unique_ptr<sql::ResultSet> rs(statement->executeQuery(
            std::string(base::sqlstring("SHOW FULL COLUMNS FROM !.!", 0) << schema_name << view)));

          while (rs->next()) {
            _databaseSymbols.addNewSymbol<ColumnSymbol>(viewSymbol, rs->getString(1), nullptr);
          }
        }
      }

      for (auto procedure : *procedures) {
        _databaseSymbols.addNewSymbol<StoredRoutineSymbol>(schemaSymbol, procedure, nullptr);
      }

      for (auto function : *functions) {
        _databaseSymbols.addNewSymbol<StoredRoutineSymbol>(schemaSymbol, function, nullptr);
      }

      if (statement != nullptr) {
        auto metaInfo = _usr_dbc_conn->ref->getMetaData();
        if (hasPerformanceSchema && (metaInfo->getDatabaseMajorVersion() > 7
            || (metaInfo->getDatabaseMajorVersion() == 5 && metaInfo->getDatabaseMinorVersion() > 6))) {
          std::unique_ptr<sql::ResultSet> rs(
            statement->executeQuery("SELECT VARIABLE_NAME FROM performance_schema.user_variables_by_thread")
          );

          while (rs->next()) {
            _databaseSymbols.addNewSymbol<UserVariableSymbol>(nullptr, "@" + rs->getString(1), nullptr);
          }
        }
      }

      return;
    }
  }
}

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

void SqlEditorForm::cache_active_schema_name() {
  std::string schema = _usr_dbc_conn->ref->getSchema();
  _usr_dbc_conn->active_schema = schema;
  _aux_dbc_conn->active_schema = schema;

  exec_sql_task->execute_in_main_thread(std::bind(&SqlEditorForm::update_editor_title_schema, this, schema), false,
                                        true);
}

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

void SqlEditorForm::active_schema(const std::string &value) {
  try {
    if (value == active_schema())
      return;

    {
      RecMutexLock aux_dbc_conn_mutex(ensure_valid_aux_connection());
      if (!value.empty())
        _aux_dbc_conn->ref->setSchema(value);
      _aux_dbc_conn->active_schema = value;
    }

    {
      RecMutexLock usr_dbc_conn_mutex(ensure_valid_usr_connection());
      if (!value.empty())
        _usr_dbc_conn->ref->setSchema(value);
      _usr_dbc_conn->active_schema = value;
    }

    if (_tabdock) {
      // set current schema for the editors to notify the autocompleter
      for (int c = _tabdock->view_count(), i = 0; i < c; i++) {
        SqlEditorPanel *panel = sql_editor_panel(i);
        if (panel)
          panel->editor_be()->set_current_schema(value);
      }
    }
    _live_tree->on_active_schema_change(value);
    // remember active schema
    _connection->parameterValues().gset("DbSqlEditor:LastDefaultSchema", value);

    update_editor_title_schema(value);

    if (value.empty())
      bec::GRTManager::get()->replace_status_text(_("Active schema was cleared"));
    else
      bec::GRTManager::get()->replace_status_text(strfmt(_("Active schema changed to %s"), value.c_str()));

    grt::GRT::get()->call_module_function("Workbench", "saveConnections", grt::BaseListRef());
  }
  CATCH_ANY_EXCEPTION_AND_DISPATCH(_("Set active schema"))
}

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

db_mgmt_RdbmsRef SqlEditorForm::rdbms() {
  if (_connection.is_valid()) {
    if (!_connection->driver().is_valid())
      throw std::runtime_error("Connection has invalid driver, check connection parameters.");
    return db_mgmt_RdbmsRef::cast_from(_connection->driver()->owner());
  } else
    return db_mgmt_RdbmsRef::cast_from(grt::GRT::get()->get("/wb/rdbmsMgmt/rdbms/0/"));
}

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

int SqlEditorForm::count_connection_editors(const std::string &conn_name) {
  int count = 0;
  std::weak_ptr<SqlEditorForm> editor;

  std::list<std::weak_ptr<SqlEditorForm>>::iterator index, end;

  end = _wbsql->get_open_editors()->end();
  for (index = _wbsql->get_open_editors()->begin(); index != end; index++) {
    SqlEditorForm::Ref editor((*index).lock());
    if (editor->_connection.is_valid()) {
      std::string editor_connection = editor->_connection->name();
      if (editor_connection == conn_name)
        count++;
    }
  }

  return count;
}

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

std::string SqlEditorForm::create_title() {
  std::string caption;
  std::string editor_connection = get_session_name();
  if (_connection.is_valid()) {
    if (!editor_connection.empty())
      caption += strfmt("%s", editor_connection.c_str());
    else {
      if (_connection->driver()->name() == "MysqlNativeSocket")
        caption += "localhost";
      else
        caption += strfmt("%s", truncate_text(editor_connection, 21).c_str());
    }

    // only show schema name if there's more than 1 tab to the same connection, to save space
    if (!_usr_dbc_conn->active_schema.empty() && count_connection_editors(editor_connection) > 1)
      caption += strfmt(" (%s)", truncate_text(_usr_dbc_conn->active_schema, 20).c_str());

    if (_connection_details.find("dbmsProductVersion") != _connection_details.end() &&
        !bec::is_supported_mysql_version(_connection_details["dbmsProductVersion"]))
      caption += " - Warning - not supported";
  } else
    caption = editor_connection;
  return caption;
}

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

void SqlEditorForm::update_title() {
  std::string temp_title = create_title();
  if (_title != temp_title) {
    _title = temp_title;
    title_changed();
  }
}

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

int SqlEditorForm::getTunnelPort() const {
  if (_tunnel)
    return _tunnel->getConfig().localport;
  return -1;
}

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

GrtVersionRef SqlEditorForm::rdbms_version() const {
  return _version;
}

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

/**
 * Returns the current server version (or a reasonable default if not connected) in compact form
 * as needed for parsing on various occasions (context help, auto completion, error parsing).
 */
int SqlEditorForm::server_version() {
  GrtVersionRef version = rdbms_version();

  // Create a server version of the form "Mmmrr" as long int for quick comparisons.
  if (version.is_valid())
    return (int)(version->majorNumber() * 10000 + version->minorNumber() * 100 + version->releaseNumber());
  else
    return 50503;
}

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

/**
 * Returns a list of valid charsets for this connection as needed for parsing.
 */
std::set<std::string> SqlEditorForm::valid_charsets() {
  if (_charsets.empty()) {
    grt::ListRef<db_CharacterSet> list = rdbms()->characterSets();
    for (std::size_t i = 0; i < list->count(); i++)
      _charsets.insert(base::tolower(*list[i]->name()));

    // 3 character sets were added in version 5.5.3. Remove them from the list if the current version
    // is lower than that.
    if (server_version() < 50503) {
      _charsets.erase("utf8mb4");
      _charsets.erase("utf16");
      _charsets.erase("utf32");
    }
  }
  return _charsets;
}

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

bool SqlEditorForm::save_snippet() {
  SqlEditorPanel *panel = active_sql_editor_panel();
  if (!panel)
    return false;
  std::string text;
  std::size_t start, end;
  if (panel->editor_be()->selected_range(start, end))
    text = panel->editor_be()->selected_text();
  else
    text = panel->editor_be()->current_statement();

  if (text.empty())
    return false;

  DbSqlEditorSnippets::get_instance()->add_snippet("", text, true);
  bec::GRTManager::get()->replace_status_text("SQL saved to snippets list.");

  _side_palette->refresh_snippets();

  bec::GRTManager::get()->run_once_when_idle(this, std::bind(&QuerySidePalette::edit_last_snippet, _side_palette));

  return true;
}

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

bool SqlEditorForm::can_close() {
  return can_close_(true);
}

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

bool SqlEditorForm::can_close_(bool interactive) {
  if (exec_sql_task && exec_sql_task->is_busy()) {
    bec::GRTManager::get()->replace_status_text(_("Cannot close SQL IDE while being busy"));
    return false;
  }

  if (!bec::UIForm::can_close())
    return false;

  _live_tree->prepare_close();
  bec::GRTManager::get()->set_app_option("DbSqlEditor:ActiveSidePaletteTab",
                                         grt::IntegerRef(_side_palette->get_active_tab()));

  bool check_scratch_editors = true;
  bool save_workspace_on_close = false;

  // if Save of workspace on close is enabled, we don't need to check whether there are unsaved
  // SQL editors but other stuff should be checked.
  grt::ValueRef option(bec::GRTManager::get()->get_app_option("workbench:SaveSQLWorkspaceOnClose"));
  if (option.is_valid() && *grt::IntegerRef::cast_from(option)) {
    save_workspace_on_close = true;
    check_scratch_editors = false;
  }
  bool editor_needs_review = false;
  if (interactive) {
    ConfirmSaveDialog dialog(
      0, "Close SQL Editor",
      "The following files/resultsets have unsaved changes.\nDo you want to review these changes before closing?");
    for (int i = 0; i < sql_editor_count(); i++) {
      SqlEditorPanel *panel = sql_editor_panel(i);
      if (!panel)
        continue;

      bool check_editor = !panel->is_scratch() || check_scratch_editors;
      if (panel->filename().empty() && save_workspace_on_close)
        check_editor = false;

      if (panel->is_dirty() && check_editor) {
        editor_needs_review = true;
        dialog.add_item("Script Buffers", panel->get_title());
      }

      std::list<SqlEditorResult *> rset(panel->dirty_result_panels());
      for (auto *r : rset) {
        dialog.add_item("Resultset", r->caption());
      }
    }

    bool review = false;
    if (dialog.change_count() > 1) {
      switch (dialog.run()) {
        case ConfirmSaveDialog::ReviewChanges:
          review = true;
          break;

        case ConfirmSaveDialog::DiscardChanges:
          review = false;
          break;

        case ConfirmSaveDialog::Cancel:
          return false;
      }
    } else if (dialog.change_count() == 1)
      review = true;

    // review changes 1 by 1
    if (review && editor_needs_review) {
      _closing = true;
      for (int i = 0; i < sql_editor_count(); i++) {
        SqlEditorPanel *panel = sql_editor_panel(i);
        if (panel && !panel->can_close()) {
          _closing = false;
          return false;
        }
      }
    }
  } else { // !interactive, return false if there's any unsaved edits in editor or resultsets
    for (int i = 0; i < sql_editor_count(); i++) {
      SqlEditorPanel *panel = sql_editor_panel(i);
      if (panel) {
        if (editor_needs_review && panel->is_dirty())
          return false;

        if (!panel->dirty_result_panels().empty())
          return false;
      }
    }
  }

  return true;
}

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

void SqlEditorForm::check_external_file_changes() {
  for (int i = 0; i < sql_editor_count(); i++) {
    SqlEditorPanel *panel = sql_editor_panel(i);
    if (panel)
      panel->check_external_file_changes();
  }
}

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

void SqlEditorForm::update_editor_title_schema(const std::string &schema) {
  _live_tree->on_active_schema_change(schema);

  // Gets the editor label including the schema name only if
  // the number of opened editors to the same host is > 1
  update_title();
}

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

/**
 * Called whenever a connection to a server is opened, whether it succeeds or not.
 *
 * Call this when a connection to the server is opened. If the connection succeeded, pass 0 as
 * the error and if it fails, pass the error code.
 *
 * The error will be used to determine whether the connection failed because the server is possibly
 * down (or doesn't exist) or some other reason (like wrong password).
 */
void SqlEditorForm::note_connection_open_outcome(int error) {
  ServerState newState;
  switch (error) {
    case 0:
      newState = RunningState; // success = running;
      break;
    case 2002: // CR_CONNECTION_ERROR
    case 2003: // CR_CONN_HOST_ERROR
      newState = PossiblyStoppedState;
      break;
    case 2013: // Lost packet blabla, can happen on failure when using ssh tunnel
      newState = PossiblyStoppedState;
      break;
    default:
      // there may be other errors that could indicate server stopped and maybe
      // some errors that can't tell anything about the server state
      newState = RunningState;
      break;
  }

  if (_last_server_running_state != newState && newState != UnknownState) {
    grt::DictRef info(true);
    _last_server_running_state = newState;

    if (newState == RunningState)
      info.gset("state", 1);
    else if (newState == OfflineState)
      info.gset("state", -1);
    else
      info.gset("state", 0);

    info.set("connection", connection_descriptor());

    logDebug2("Notifying server state change of %s to %s\n", connection_descriptor()->hostIdentifier().c_str(),
              (newState == RunningState || newState == OfflineState) ? "running" : "not running");
    GRTNotificationCenter::get()->send_grt("GRNServerStateChanged", grtobj(), info);
  }
}

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