/*
 * Copyright (c) 2014, 2024, 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
 */
#define __STDC_FORMAT_MACROS 1

#include "shellcore/shell_resultset_dumper.h"

#include <algorithm>
#include <cinttypes>
#include <deque>

#include "ext/linenoise-ng/include/linenoise.h"
#include "mysqlshdk/include/shellcore/base_shell.h"
#include "mysqlshdk/include/shellcore/console.h"
#include "mysqlshdk/libs/db/column.h"
#include "mysqlshdk/libs/db/row_copy.h"
#include "mysqlshdk/libs/utils/dtoa.h"
#include "mysqlshdk/libs/utils/strformat.h"
#include "mysqlshdk/libs/utils/utils_encoding.h"  // base64 encoding utilities
#include "mysqlshdk/libs/utils/utils_json.h"
#include "shellcore/interrupt_handler.h"
#include "utils/utils_general.h"
#include "utils/utils_string.h"

#define MAX_DISPLAY_LENGTH 1024

// max # of rows to pre-fetch when dumping resultsets with table formatting,
// in order to calculate column widths
static constexpr const int k_pre_fetch_result_rows = 1000;

namespace mysqlsh {

/* Calculates the required buffer size and display size considering:
 * - Some single byte characters may require injection of escaped sequence \\
 * - Some multibyte characters are displayed in the space of a single character
 * - Some multibyte characters are displayed in the space of two characters
 *
 * For the reasons above, the real buffer required to store the formatted text
 * might be bigger than the original length and the space required on screen
 * might be either lower (mb chars) or bigger (injected escapes) than the
 * original length.
 *
 * This function returns a tuple containing:
 * - Real display character count
 * - Real byte size
 */
std::tuple<size_t, size_t> get_utf8_sizes(const char *text, size_t length,
                                          Print_flags flags) {
  size_t char_count = 0;
  size_t byte_count = 0;

  const char *index = text;
  const char *end = index + length;

#ifdef _WIN32
  // By default, we assume no multibyte content on the string and
  // no escaped characters.
  bool is_multibyte = false;
  byte_count = length;
  char_count = length;

  if (length) {
    // Calculates the required size for the buffer
    int required = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text,
                                       length, nullptr, 0);
    // Required would be 0 in case of error, possible errors include:
    // ERROR_INSUFFICIENT_BUFFER. A supplied buffer size was not large enough,
    // or it was incorrectly set to NULL. ERROR_INVALID_FLAGS. The values
    // supplied for flags were not valid. ERROR_INVALID_PARAMETER. Any of the
    // parameter values was invalid. ERROR_NO_UNICODE_TRANSLATION. Invalid
    // Unicode was found in a string.
    //
    // We know ERROR_INSUFFICIENT_BUFFER is not since we were calculating the
    // buffer size We assume ERROR_INVALID_FLAGS and ERROR_INVALID_PARAMETER are
    // not, since the function succeeds in most of the cases. So only posibility
    // is ERROR_NO_UNICODE_TRANSLATION which would be the case i.e. for binary
    // data. In such case the function simply exits returning the original
    // lengths.
    if (required > 0) {
      std::wstring wstr;
      wstr = std::wstring(required, 0);
      required = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text,
                                     length, &wstr[0], required);

      // If the final number of characters is different from the input
      // length, it means there are characters that are multibyte
      if (required != length) {
        is_multibyte = true;

        // Character count will be totally calculated on the loop
        // not only escaped characters
        char_count = 0;
      }

      auto character = wstr.begin();

      // This loop is used to calculates two things
      // 1) The "screen spaces" to be required by the string
      //    - Some are injected spaces
      //    - For multibyte strings, it includes width calculation
      // 2) The injected bytes due to escape handling
      while (character != wstr.end()) {
        if (*character == '\0') {
          if (flags.is_set(Print_flag::PRINT_0_AS_SPC)) {
            // If it's multibyte, length is being calculated so we need
            // to add 1 on this case, otherwise it is already considered
            if (is_multibyte) char_count++;
          } else if (flags.is_set(Print_flag::PRINT_0_AS_ESC)) {
            char_count += is_multibyte ? 2 : 1;
            byte_count++;
          } else {
            // If not multibyte, we need to remove one char since unescaped \0
            // should not occupy space
            if (!is_multibyte) char_count--;
          }
        } else if (flags.is_set(Print_flag::PRINT_CTRL) &&
                   (*character == '\t' || *character == '\n' ||
                    *character == '\\')) {
          char_count += is_multibyte ? 2 : 1;
          byte_count++;
        } else if (is_multibyte) {
          // Get the character width
          int size = getWcwidth(*character);

          // We ignore control characters which may return -1
          // since no character will really remove screen space
          if (size > 0) char_count += size;
        }

        character++;
      }
    }
  }

#else
  std::mblen(NULL, 0);
  while (index < end) {
    int width = std::mblen(index, end - index);

    // handles single byte characters
    if (width == 1) {
      // Controls characters to be printed add one extra char to the output
      if (flags.is_set(Print_flag::PRINT_CTRL) &&
          (*index == '\t' || *index == '\n' || *index == '\\')) {
        char_count++;
        byte_count++;
      }

      // The character itself
      char_count++;
      byte_count++;
      index++;
    } else if (width == 0) {
      // handles the \0 character
      index += 1;

      // Printed as a space
      if (flags.is_set(Print_flag::PRINT_0_AS_SPC)) {
        char_count += 1;
        byte_count += 1;

        // Escape injection to be printed as \\0
      } else if (flags.is_set(Print_flag::PRINT_0_AS_ESC)) {
        char_count += 2;
        byte_count += 2;
      } else {
        // No char_count but byte is needed
        byte_count++;
      }
    } else if (width == -1) {
      // If a so weird character was found, then it makes no sense to continue
      // processing since the final measure will not be accurate anyway, so we
      // return the original data
      // This could be the case on any binary data column.
      char_count = length;
      byte_count = length;
      break;
    } else {
      // Multibyte characters handling
      // We need to find out whether they are printed in single or double space
      wchar_t mbchar;
      int size = 0;
      if (std::mbtowc(&mbchar, index, width) > 0) {
        size = getWcwidth(mbchar);
        // We ignore control characters which may return -1
        // since no character will really remove screen space
        if (size < 0) size = 0;
      } else {
        size = 1;
      }

      char_count += size;
      byte_count += width;
      index += width;
    }
  }
#endif

  std::tuple<size_t, size_t> ret_val{char_count, byte_count};

  return ret_val;
}

namespace {

enum class ResultFormat { VERTICAL, TABBED, TABLE };

class Field_formatter {
 public:
  Field_formatter(ResultFormat format, const mysqlshdk::db::Column &column)
      : m_allocated(0),
        m_max_display_length(0),
        m_max_buffer_length(0),
        m_max_mb_holes(0),
        m_format(format),
        m_type(column.get_type()),
        m_is_numeric(column.is_numeric()) {
    m_zerofill = column.is_zerofill() ? column.get_length() : 0;

    switch (m_format) {
      case ResultFormat::TABBED:
        m_flags = Print_flags(Print_flag::PRINT_0_AS_ESC);
        m_flags.set(Print_flag::PRINT_CTRL);
        m_align_right = false;
        break;
      case ResultFormat::VERTICAL:
        m_flags = Print_flags(Print_flag::PRINT_0_AS_SPC);
        m_align_right = false;
        break;
      case ResultFormat::TABLE:
        m_flags = Print_flags(Print_flag::PRINT_0_AS_SPC);
        m_align_right = column.is_zerofill() || column.is_numeric();

        // Gets the column name display/buffer sizes
        auto col_sizes =
            get_utf8_sizes(column.get_column_label().c_str(),
                           column.get_column_label().length(),
                           Print_flags(Print_flag::PRINT_0_AS_ESC));

        m_max_mb_holes = std::get<1>(col_sizes) - std::get<0>(col_sizes);
        m_max_display_length = std::max(std::get<0>(col_sizes), m_zerofill);
        m_max_buffer_length = std::get<1>(col_sizes);
        break;
    }
  }

  void process(const mysqlshdk::db::IRow *row, size_t index) {
    // This function is meant to be called only for tables
    assert(m_format == ResultFormat::TABLE);
    size_t dlength{0};
    size_t blength{0};

    if (row->is_null(index)) {
      dlength = blength = 4;
    } else if (m_is_numeric) {
      auto str = get_number_string(row, index);
      dlength = blength = str.length();
    } else if (m_type == mysqlshdk::db::Type::Bit) {
      dlength = blength =
          shcore::bits_to_string_hex_size(std::get<1>(row->get_bit(index))) + 2;
    } else if (m_type == mysqlshdk::db::Type::Bytes) {
      // TODO (anyone): Implement support for --skip-binary-as-hex
      auto data = row->get_string_data(index);
      dlength = blength = 2 + data.second * 2;
    } else {
      auto data = row->get_as_string(index);
      auto fsizes = get_utf8_sizes(data.c_str(), data.length(), m_flags);
      dlength = std::get<0>(fsizes);
      blength = std::get<1>(fsizes);
    }

    m_max_mb_holes = std::max<size_t>(m_max_mb_holes, blength - dlength);
    m_max_display_length = std::max<size_t>(m_max_display_length, dlength);
    m_max_buffer_length = std::max<size_t>(m_max_buffer_length, blength);
  }

  ~Field_formatter() = default;

  std::string get_number_string(const mysqlshdk::db::IRow *row, size_t index) {
    if (m_type == mysqlshdk::db::Type::Float) {
      return shcore::ftoa(row->get_float(index));
    } else if (m_type == mysqlshdk::db::Type::Integer) {
      return std::to_string(row->get_int(index));
    } else if (m_type == mysqlshdk::db::Type::UInteger) {
      return std::to_string(row->get_uint(index));
    } else if (m_type == mysqlshdk::db::Type::Double) {
      return shcore::dtoa(row->get_double(index));
    } else {
      return row->get_as_string(index);
    }
  }

  bool put(const mysqlshdk::db::IRow *row, size_t index) {
    reset();

    std::string tmp;
    const char *data;
    size_t length;

    size_t display_size;
    size_t buffer_size;

    if (row->is_null(index)) {
      data = "NULL";
      display_size = buffer_size = length = 4;
    } else if (m_is_numeric) {
      tmp = get_number_string(row, index);

      if (m_zerofill > tmp.length()) {
        tmp = std::string(m_zerofill - tmp.length(), '0').append(tmp);
      }
      data = tmp.data();
      display_size = buffer_size = length = tmp.length();
    } else if (m_type == mysqlshdk::db::Type::Bit) {
      auto [bit_value, bit_size] = row->get_bit(index);
      tmp = shcore::bits_to_string_hex(bit_value, bit_size);

      data = tmp.data();
      display_size = buffer_size = length = tmp.length();
    } else if (m_type == mysqlshdk::db::Type::Bytes) {
      std::tie(data, length) = row->get_string_data(index);

      tmp = shcore::string_to_hex({data, length});
      data = tmp.data();
      length = tmp.size();

      std::tie(display_size, buffer_size) =
          get_utf8_sizes(data, length, m_flags);
    } else {
      tmp = row->get_as_string(index);
      data = tmp.data();
      length = tmp.length();

      std::tie(display_size, buffer_size) =
          get_utf8_sizes(data, length, m_flags);
    }

    if (!append(data, length, display_size, buffer_size)) {
      if (m_is_numeric || m_type == mysqlshdk::db::Type::Bit) {
        // if a number is larger than expected (e.g. floating pt with lots of
        // decimals)
        m_buffer.assign(data, length);
      } else {
        return false;
      }
    }
    return true;
  }

  const std::string &str() const { return m_buffer; }
  size_t get_max_display_length() const { return m_max_display_length; }
  size_t get_max_buffer_length() const { return m_max_buffer_length; }

 private:
  std::string m_buffer;
  size_t m_allocated;
  size_t m_zerofill;
  bool m_align_right;

  size_t m_max_display_length;
  size_t m_max_buffer_length;
  size_t m_max_mb_holes;

  ResultFormat m_format;
  Print_flags m_flags;
  mysqlshdk::db::Type m_type;
  bool m_is_numeric;

  void reset() {
    // sets the buffer only once
    if (m_buffer.empty()) {
      if (m_format == ResultFormat::TABLE) {
        m_allocated = std::max<size_t>(m_max_display_length,
                                       m_max_buffer_length + m_max_mb_holes);

        if (m_allocated > MAX_DISPLAY_LENGTH) m_allocated = MAX_DISPLAY_LENGTH;
      } else {
        m_allocated = MAX_DISPLAY_LENGTH;
      }
    }
    m_buffer.resize(m_allocated);

    memset(&m_buffer[0], ' ', m_buffer.size());
  }

  bool append(const char *text, size_t length, size_t display_size,
              size_t buffer_size) {
    if (buffer_size > m_allocated) return false;

    size_t next_index = 0;
    if (m_format == ResultFormat::TABLE) {
      if (m_align_right && m_max_display_length > display_size) {
        next_index = m_max_display_length - display_size;
      }
    }

    auto buffer = &m_buffer[0];
    for (size_t index = 0; index < length; index++) {
      if (m_flags.is_set(Print_flag::PRINT_0_AS_ESC) && text[index] == '\0') {
        buffer[next_index++] = '\\';
        buffer[next_index++] = '0';
      } else if (m_flags.is_set(Print_flag::PRINT_0_AS_SPC) &&
                 text[index] == '\0') {
        buffer[next_index++] = ' ';
      } else if (m_flags.is_set(Print_flag::PRINT_CTRL) &&
                 text[index] == '\t') {
        buffer[next_index++] = '\\';
        buffer[next_index++] = 't';
      } else if (m_flags.is_set(Print_flag::PRINT_CTRL) &&
                 text[index] == '\n') {
        buffer[next_index++] = '\\';
        buffer[next_index++] = 'n';
      } else if (m_flags.is_set(Print_flag::PRINT_CTRL) &&
                 text[index] == '\\') {
        buffer[next_index++] = '\\';
        buffer[next_index++] = '\\';
      } else {
        buffer[next_index++] = text[index];
      }
    }

    if (m_format == ResultFormat::TABLE) {
      // If some multibyte characters were found, we need to truncate the buffer
      // adding the 'lost' characters
      buffer[std::min(m_allocated,
                      m_max_display_length + (buffer_size - display_size))] = 0;
    } else {
      m_buffer.resize(next_index);
    }

    return true;
  }
};

class Console_printer : public Resultset_printer {
 public:
  Console_printer() : m_console(mysqlsh::current_console()) {}

  void print(const std::string &s) override { m_console->print(s); }

  void println(const std::string &s) override { m_console->println(s); }

  void raw_print(const std::string &s) override {
    m_console->raw_print(s, mysqlsh::Output_stream::STDOUT, false);
  }

 private:
  std::shared_ptr<IConsole> m_console;
};

class String_printer : public Resultset_printer {
 public:
  String_printer() = default;

  void print(const std::string &s) override { raw_print(s); }

  void println(const std::string &s) override {
    print(s);
    print("\n");
  }

  void raw_print(const std::string &s) override { m_output += s; }

  void reset() override { m_output.clear(); }

  std::string data() const override { return m_output; }

 private:
  std::string m_output;
};

}  // namespace

Resultset_dumper_base::Resultset_dumper_base(
    mysqlshdk::db::IResult *target, std::unique_ptr<Resultset_printer> printer,
    const std::string &wrap_json, const std::string &format,
    bool show_column_type_info)
    : m_result(target),
      m_wrap_json(wrap_json),
      m_format(format),
      m_printer(std::move(printer)),
      m_show_column_type_info(show_column_type_info) {
  if (m_format == "ndjson") m_format = "json/raw";
}

Resultset_dumper::Resultset_dumper(mysqlshdk::db::IResult *target,
                                   bool show_column_type_info)
    : Resultset_dumper(target,
                       mysqlsh::current_shell_options()->get().wrap_json,
                       mysqlsh::current_shell_options()->get().result_format,
                       mysqlsh::current_shell_options()->get().show_warnings,
                       mysqlsh::current_shell_options()->get().interactive,
                       show_column_type_info) {}

Resultset_dumper::Resultset_dumper(mysqlshdk::db::IResult *target,
                                   const std::string &wrap_json,
                                   const std::string &format,
                                   bool show_warnings, bool show_stats,
                                   bool show_column_type_info)
    : Resultset_dumper_base(target, std::make_unique<Console_printer>(),
                            wrap_json, format, show_column_type_info),
      m_show_warnings(show_warnings),
      m_show_stats(show_stats) {}

size_t Resultset_dumper::dump(const std::string &item_label, bool is_query,
                              bool is_doc_result) {
  std::string output;
  size_t total_count = 0;
  shcore::Interrupt_handler intr([this]() {
    m_cancelled = true;
    return true;
  });

  bool first = true;
  bool wrapping_json = m_wrap_json != "off";
  do {
    if (wrapping_json) {
      total_count = dump_json(item_label, is_doc_result);
    } else {
      size_t count = 0;
      // Prints a blank line between multi-results
      if (first) {
        first = false;
      } else {
        m_printer->print("\n");
      }

      if (m_result->has_resultset()) {
        if (show_column_type_info()) dump_metadata();

        if (is_doc_result || m_format.find("json") != std::string::npos)
          count = dump_documents(is_doc_result);
        else if (m_format == "vertical")
          count = dump_vertical();
        else if (m_format == "table")
          count = dump_table();
        else
          count = dump_tabbed();

        total_count += count;

        if (count)
          output =
              shcore::str_format("%zu %s%s in set", count, item_label.c_str(),
                                 (count == 1 ? "" : "s"));
        else
          output = "Empty set";
      } else if (m_show_stats && !is_query) {
        // Starts only make sense on non read only operations
        output = get_affected_stats(item_label);
      }

      // This information output is only printed in interactive mode
      int warning_count = 0;
      if (m_show_stats) {
        warning_count = get_warning_and_execution_time_stats(&output);

        m_printer->print(output);
      }

      if (!is_query) {
        std::string info = m_result->get_info();
        if (!info.empty()) {
          info = "\n" + info + "\n";
          m_printer->print(info);
        }
      }

      // Prints the warnings if there were any
      if (warning_count && m_show_warnings) dump_warnings();
    }
  } while (m_result->next_resultset() && !m_cancelled);

  if (m_cancelled) {
    m_printer->println(
        "Result printing interrupted, rows may be missing from the output.");
  } else if (!wrapping_json) {
    const auto statement_id = m_result->get_statement_id();
    if (!statement_id.empty()) {
      m_printer->println("Statement ID: " + statement_id);
    }
  }

  return total_count;
}

/**
 * This utility function creates a JSON document including all the fields in a
 * given row and appends it to a JSON_dumper object.
 */
void dump_json_row(shcore::JSON_dumper *dumper,
                   const std::vector<mysqlshdk::db::Column> &metadata,
                   const mysqlshdk::db::IRow *row) {
  dumper->start_object();

  for (size_t col_index = 0; col_index < metadata.size(); col_index++) {
    auto column = metadata[col_index];

    dumper->append_string(column.get_column_label());
    auto type = column.get_type();
    if (row->is_null(col_index)) {
      dumper->append_null();
    } else if (mysqlshdk::db::is_string_type(type)) {
      if (type == mysqlshdk::db::Type::Json) {
        dumper->append_json(row->get_string(col_index));
      } else if (type == mysqlshdk::db::Type::Bytes) {
        auto data = row->get_string_data(col_index);
        std::string encoded;
        size_t binary_limit = data.second;
        if (mysqlsh::current_shell_options()->get().binary_limit > 0) {
          binary_limit = std::min(
              data.second,
              mysqlsh::current_shell_options()->get().binary_limit + 1);
        }

        shcore::encode_base64(
            static_cast<const unsigned char *>(
                static_cast<const void *>(data.first)),
            // At most binary-limit + 1 bytes shuold be sent, when the extra
            // byte is sent, it will be an indicator for the consumer of the
            // data that a truncation happened
            binary_limit, &encoded);
        dumper->append_string(encoded);
      } else {
        auto data = row->get_as_string(col_index);
        dumper->append_string(data);
      }
    } else if (type == mysqlshdk::db::Type::Integer) {
      dumper->append_int64(row->get_int(col_index));
    } else if (type == mysqlshdk::db::Type::UInteger) {
      dumper->append_uint64(row->get_uint(col_index));
    } else if (type == mysqlshdk::db::Type::Float) {
      dumper->append_float(static_cast<double>(row->get_float(col_index)));
    } else if (type == mysqlshdk::db::Type::Double) {
      dumper->append_float(row->get_double(col_index));
    } else if (type == mysqlshdk::db::Type::Decimal) {
      dumper->append_float(static_cast<double>(row->get_float(col_index)));
    } else if (type == mysqlshdk::db::Type::Bit) {
      auto [bit_value, bit_size] = row->get_bit(col_index);
      dumper->append_string(shcore::bits_to_string_hex(bit_value, bit_size));
    }
  }
  dumper->end_object();
}

/**
 * Dumps a JSON document for each row/document contained on the result
 * being processed.
 */
size_t Resultset_dumper_base::dump_documents(bool is_doc_result) {
  const auto &metadata = m_result->get_metadata();
  auto row = m_result->fetch_one();
  size_t row_count = 0;
  const bool as_array = m_format == "json/array";
  const bool pretty = m_format != "json/raw" && m_format != "json/array";

  if (!row) return row_count;

  if (as_array) m_printer->raw_print("[\n");
  while (row) {
    shcore::JSON_dumper dumper(
        pretty, mysqlsh::current_shell_options()->get().binary_limit);

    if (row_count > 0) {
      if (as_array)
        m_printer->raw_print(",\n");
      else
        m_printer->raw_print("\n");
    }

    if (is_doc_result)
      dumper.append_json(row->get_string(0));
    else
      dump_json_row(&dumper, metadata, row);

    m_printer->raw_print(dumper.str());

    row_count++;
    row = m_result->fetch_one();
  }
  m_printer->raw_print("\n");
  if (as_array) m_printer->raw_print("]\n");

  return row_count;
}

size_t Resultset_dumper_base::dump_tabbed() {
  const auto &metadata = m_result->get_metadata();
  auto row = m_result->fetch_one();
  size_t row_index = 0;

  if (!row) return row_index;

  size_t index = 0;
  size_t field_count = metadata.size();
  std::vector<std::string> formats(field_count, "%-");
  std::vector<Field_formatter> fmt;

  // Prints the initial separator line and the column headers
  for (index = 0; index < field_count; index++) {
    auto column = metadata[index];

    fmt.emplace_back(ResultFormat::TABBED, column);
    m_printer->print(column.get_column_label().c_str());
    m_printer->print(index < (field_count - 1) ? "\t" : "\n");
  }

  // Now prints the records
  while (row && !m_cancelled) {
    for (size_t field_index = 0; field_index < field_count; field_index++) {
      auto column = metadata[field_index];
      if (fmt[field_index].put(row, field_index)) {
        m_printer->print(fmt[field_index].str());
      } else {
        mysqlshdk::db::is_string_type(column.get_type());
        m_printer->print(row->get_string(field_index));
      }
      m_printer->print(field_index < (field_count - 1) ? "\t" : "\n");
    }

    row = m_result->fetch_one();
    row_index++;
  }

  return row_index;
}

size_t Resultset_dumper_base::dump_vertical() {
  return format_vertical(true, true, 0);
}

size_t Resultset_dumper_base::format_vertical(bool has_header, bool align_right,
                                              size_t min_label_width) {
  const auto &metadata = m_result->get_metadata();

  const std::string star_separator(27, '*');

  std::vector<Field_formatter> fmt;

  // Calculate length of a longest column description, used to right align
  // column descriptions
  std::size_t max_col_len = min_label_width;
  for (const auto &column : metadata) {
    max_col_len = std::max(max_col_len, column.get_column_label().length());
    fmt.emplace_back(ResultFormat::VERTICAL, column);
  }

  auto row = m_result->fetch_one();
  size_t row_index = 0;
  while (row && !m_cancelled) {
    if (has_header) {
      std::string row_header = star_separator + " " +
                               std::to_string(row_index + 1) + ". row " +
                               star_separator + "\n";

      m_printer->print(row_header);
    }

    for (size_t col_index = 0; col_index < metadata.size(); col_index++) {
      auto column = metadata[col_index];

      std::string padding(max_col_len - column.get_column_label().size(), ' ');
      std::string label = column.get_column_label() + ": ";

      if (align_right) {
        label = padding + label;
      } else {
        label += padding;
      }

      m_printer->print(label);
      if (fmt[col_index].put(row, col_index)) {
        m_printer->print(fmt[col_index].str());
      } else {
        assert(mysqlshdk::db::is_string_type(column.get_type()));
        m_printer->print(row->get_string(col_index));
      }
      m_printer->raw_print("\n");
    }

    row = m_result->fetch_one();
    row_index++;
  }
  return row_index;
}

/**
 * Creates a JSON Document including all the information in the result being
 * processd, this includes:
 *
 * - Metadata
 * - Rows
 * - Statistics
 * - Warnings
 *
 * This function is used when JSON Wrapping is turned ON
 */
std::string Resultset_dumper_base::format_json(const std::string &item_label,
                                               bool is_doc_result, bool pretty,
                                               int *row_count) {
  assert(row_count);
  shcore::JSON_dumper dumper(
      pretty, mysqlsh::current_shell_options()->get().binary_limit);

  dumper.start_object();
  dumper.append_string("hasData");
  dumper.append_bool(m_result->has_resultset());

  dumper.append_string(item_label + "s");
  dumper.start_array();

  *row_count = 0;
  if (m_result->has_resultset()) {
    if (show_column_type_info()) dump_metadata();

    const auto &metadata = m_result->get_metadata();
    auto row = m_result->fetch_one();
    while (row) {
      if (is_doc_result) {
        dumper.append_json(row->get_string(0));
      } else {
        dump_json_row(&dumper, metadata, row);
      }
      (*row_count)++;
      row = m_result->fetch_one();
    }
  }

  dumper.end_array();

  dumper.append_string("executionTime");
  dumper.append_string(
      mysqlshdk::utils::format_seconds(m_result->get_execution_time()));
  dumper.append_string("affectedItemsCount");
  dumper.append_uint64(m_result->get_affected_row_count());
  dumper.append_string("warningsCount");
  dumper.append_uint64(m_result->get_warning_count());
  dumper.append_string("warnings");
  dumper.start_array();
  auto warning = m_result->fetch_one_warning();
  while (warning) {
    std::string level;
    switch (warning->level) {
      case mysqlshdk::db::Warning::Level::Note:
        level = "Note";
        break;
      case mysqlshdk::db::Warning::Level::Warn:
        level = "Warning";
        break;
      case mysqlshdk::db::Warning::Level::Error:
        level = "Error";
        break;
    }
    dumper.start_object();
    dumper.append_string("Level", level);
    dumper.append_string("Code");
    dumper.append_int(warning->code);
    dumper.append_string("Message", warning->msg);
    dumper.end_object();
    warning = m_result->fetch_one_warning();
  }
  dumper.end_array();

  dumper.append_string("info");
  dumper.append_string(m_result->get_info());

  dumper.append_string("autoIncrementValue");
  dumper.append_int64(m_result->get_auto_increment_value());

  if (const auto statement_id = m_result->get_statement_id();
      !statement_id.empty()) {
    dumper.append_string("statementId");
    dumper.append_string(statement_id);
  }

  dumper.end_object();

  return dumper.str();
}

size_t Resultset_dumper_base::dump_json(const std::string &item_label,
                                        bool is_doc_result) {
  int row_count = 0;

  m_printer->raw_print(format_json(item_label, is_doc_result,
                                   m_wrap_json == "json", &row_count));
  m_printer->raw_print("\n");

  return row_count;
}

size_t Resultset_dumper_base::dump_table() {
  const auto &metadata = m_result->get_metadata();
  std::vector<Field_formatter> fmt;
  std::vector<mysqlshdk::db::Row_copy> pre_fetched_rows;
  size_t num_records = 0;
  const size_t field_count = metadata.size();
  if (field_count == 0) return 0;

  // Updates the max_length array with the maximum length between column name,
  // min column length and column max length
  for (size_t field_index = 0; field_index < field_count; field_index++) {
    auto column = metadata[field_index];
    fmt.emplace_back(ResultFormat::TABLE, column);
  }

  pre_fetched_rows.reserve(k_pre_fetch_result_rows);
  {
    auto row = m_result->fetch_one();
    while (row && !m_cancelled) {
      pre_fetched_rows.emplace_back(*row);

      for (size_t field_index = 0; field_index < field_count; field_index++) {
        fmt[field_index].process(row, field_index);
      }

      if (pre_fetched_rows.size() >= k_pre_fetch_result_rows) break;

      row = m_result->fetch_one();
    }
  }
  if (m_cancelled || pre_fetched_rows.empty()) return 0;

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

  size_t index = 0;

  std::string separator("+");
  for (index = 0; index < field_count; index++) {
    std::string field_separator(fmt[index].get_max_display_length() + 2, '-');
    field_separator.append("+");
    separator.append(field_separator);
  }
  separator.append("\n");

  // Prints the initial separator line and the column headers
  m_printer->print(separator);
  m_printer->print("| ");
  for (index = 0; index < field_count; index++) {
    std::string format = "%-";
    format.append(std::to_string(fmt[index].get_max_display_length()));
    format.append((index == field_count - 1) ? "s |\n" : "s | ");
    auto column = metadata[index];
    m_printer->print(
        shcore::str_format(format.c_str(), column.get_column_label().c_str()));
  }
  m_printer->print(separator);

  // Print pre-fetched records
  for (const auto &row : pre_fetched_rows) {
    ++num_records;
    m_printer->print("| ");

    for (size_t field_index = 0; field_index < field_count; field_index++) {
      if (fmt[field_index].put(&row, field_index)) {
        m_printer->print(fmt[field_index].str());
      } else {
        assert(mysqlshdk::db::is_string_type(metadata[field_index].get_type()));
        if (row.get_type(field_index) == mysqlshdk::db::Type::Bytes) {
          const char *data;
          size_t length;
          std::tie(data, length) = row.get_string_data(field_index);
          m_printer->print(shcore::string_to_hex({data, length}));
        } else {
          m_printer->print(row.get_as_string(field_index));
        }
      }
      if (field_index < field_count - 1) m_printer->print(" | ");
    }
    m_printer->print(" |\n");

    if (m_cancelled) break;
  }
  // Now prints the remaining records
  if (!m_cancelled) {
    auto row = m_result->fetch_one();
    while (row && !m_cancelled) {
      ++num_records;
      m_printer->print("| ");

      for (size_t field_index = 0; field_index < field_count; field_index++) {
        if (fmt[field_index].put(row, field_index)) {
          m_printer->print(fmt[field_index].str());
        } else {
          assert(
              mysqlshdk::db::is_string_type(metadata[field_index].get_type()));
          m_printer->print(row->get_as_string(field_index));
        }
        if (field_index < field_count - 1) m_printer->print(" | ");
      }
      m_printer->print(" |\n");
      row = m_result->fetch_one();
    }
  }
  m_printer->print(separator);

  return num_records;
}

std::string Resultset_dumper::get_affected_stats(
    const std::string &item_label) {
  std::string output;

  // Some queries return -1 since affected rows do not apply to them
  int64_t affected_items = m_result->get_affected_row_count();

  if (affected_items == -1)
    output = "Query OK";
  else
    // In case of Query OK, prints the actual number of affected rows.
    output = shcore::str_format(
        "Query OK, %" PRId64 " %s affected", affected_items,
        (affected_items == 1 ? item_label : item_label + "s").c_str());

  return output;
}

int Resultset_dumper::get_warning_and_execution_time_stats(
    std::string *output_stats) {
  int warning_count = 0;

  if (m_show_stats) {
    warning_count = m_result->get_warning_count();

    if (warning_count)
      output_stats->append(shcore::str_format(", %d warning%s", warning_count,
                                              (warning_count == 1 ? "" : "s")));

    output_stats->append(" ");
    output_stats->append(shcore::str_format(
        "(%s)", mysqlshdk::utils::format_seconds(m_result->get_execution_time())
                    .c_str()));
    output_stats->append("\n");
  }

  return warning_count;
}

void Resultset_dumper_base::dump_warnings() {
  auto warning = m_result->fetch_one_warning();
  while (warning && !m_cancelled) {
    std::string type;
    switch (warning->level) {
      case mysqlshdk::db::Warning::Level::Note:
        type = "Note";
        break;
      case mysqlshdk::db::Warning::Level::Warn:
        type = "Warning";
        break;
      case mysqlshdk::db::Warning::Level::Error:
        type = "Error";
        break;
    }

    m_printer->print((shcore::str_format("%s (code %d): %s\n", type.c_str(),
                                         warning->code, warning->msg.c_str())));

    warning = m_result->fetch_one_warning();
  }
}

namespace {
void column_to_json(shcore::JSON_dumper *dumper,
                    const mysqlshdk::db::Column &column) {
  dumper->start_object();
  std::stringstream ss(to_string(column));
  std::string line;
  while (std::getline(ss, line)) {
    auto sep = line.find(":");
    assert(sep + 1 < line.length());
    dumper->append_string(line.substr(0, sep));
    dumper->append_string(shcore::str_strip(line.substr(sep + 1)));
  }
  dumper->end_object();
}
}  // namespace

std::string Resultset_dumper_base::format_json_metadata(bool pretty) {
  shcore::JSON_dumper dumper(
      pretty, mysqlsh::current_shell_options()->get().binary_limit);

  const auto &cols = m_result->get_metadata();

  dumper.start_object();
  for (std::size_t i = 0; i < cols.size(); i++) {
    dumper.append_string(shcore::str_format("Field %zu", i + 1));
    column_to_json(&dumper, cols[i]);
  }
  dumper.end_object();

  return dumper.str();
}

void Resultset_dumper_base::dump_metadata() {
  const auto &cols = m_result->get_metadata();
  if (m_wrap_json != "off") {
    m_printer->raw_print(format_json_metadata(m_wrap_json == "json"));
    m_printer->raw_print("\n");
  } else {
    for (std::size_t i = 0; i < cols.size(); i++) {
      m_printer->println(shcore::str_format("Field %zu", i + 1));
      m_printer->println(to_string(cols[i]));
    }
  }
}

Gui_resultset_dumper::Gui_resultset_dumper(mysqlshdk::db::IResult *target,
                                           const std::string &format)
    : Resultset_dumper(target, "json/raw", format, true, true, true) {}

bool Gui_resultset_dumper::show_column_type_info() const {
  return m_show_column_type_info && !shcore::str_beginswith(m_format, "json");
}

size_t Gui_resultset_dumper::dump_json(const std::string &item_label,
                                       bool is_doc_result) {
  int row_count = 0;
  if (shcore::str_beginswith(m_format, "json")) {
    auto pretty = m_format == "json/pretty";
    if (mysqlsh::current_shell_options()->get().show_column_type_info) {
      m_printer->print(format_json_metadata(pretty));
    }

    m_printer->print(
        format_json(item_label, is_doc_result, pretty, &row_count));
    m_printer->print("\n");
  } else {
    row_count = Resultset_dumper::dump_json(item_label, is_doc_result);
  }

  return row_count;
}

Resultset_writer::Resultset_writer(mysqlshdk::db::IResult *target)
    : Resultset_writer(target, std::make_unique<String_printer>(),
                       mysqlsh::current_shell_options()->get().wrap_json,
                       mysqlsh::current_shell_options()->get().result_format) {}

Resultset_writer::Resultset_writer(mysqlshdk::db::IResult *target,
                                   std::unique_ptr<Resultset_printer> printer,
                                   const std::string &wrap_json,
                                   const std::string &result_format,
                                   bool show_column_type_info)
    : Resultset_dumper_base(target, std::move(printer), wrap_json,
                            result_format, show_column_type_info) {}

std::string Resultset_writer::write_table() {
  return write([this]() { dump_table(); });
}

std::string Resultset_writer::write_vertical() {
  return write([this]() { dump_vertical(); });
}

std::string Resultset_writer::write_status() {
  return write([this]() { format_vertical(false, false, 24); });
}

std::string Resultset_writer::writer_gui() {
  return write([this]() {
    dump_metadata();
    dump_json("row", false);
  });
}

std::string Resultset_writer::write(const std::function<void()> &dump) {
  m_printer->reset();

  while (m_result->has_resultset()) {
    dump();
    m_printer->raw_print("\n");

    m_result->next_resultset();
  }

  return m_printer->data();
}

Gui_resultset_writer::Gui_resultset_writer(mysqlshdk::db::IResult *target)
    : Resultset_writer(target, std::make_unique<Console_printer>(), "json/raw",
                       "json/raw", true) {}

size_t dump_result(mysqlshdk::db::IResult *target,
                   const std::string &item_label, bool is_query,
                   bool is_doc_result,
                   const std::optional<std::string> &wrap_json,
                   const std::optional<std::string> &opt_format,
                   const std::optional<bool> &show_warnings,
                   const std::optional<bool> &show_stats,
                   const std::optional<bool> &show_column_type_info) {
  const auto &options = mysqlsh::current_shell_options()->get();

  bool gui_mode = options.gui_mode;

  std::string format = opt_format.value_or(options.result_format);

  std::shared_ptr<Resultset_dumper> dumper;
  // The GUI dumper is used in the cases that require specific formatting for
  // proper rendering in the GUI, in the case of the table
  if (gui_mode &&
      (format == "table" || shcore::str_beginswith(format, "json"))) {
    dumper = std::make_shared<Gui_resultset_dumper>(target, format);
  } else {
    dumper = std::make_shared<Resultset_dumper>(
        target, wrap_json.value_or(options.wrap_json), format,
        show_warnings.value_or(options.show_warnings),
        show_stats.value_or(options.interactive),
        show_column_type_info.value_or(options.show_column_type_info));
  }

  return dumper->dump(item_label, is_query, is_doc_result);
}

}  // namespace mysqlsh
