/*
   Copyright (c) 2001, 2015, Oracle and/or its affiliates. All rights reserved.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; version 2 of the License.

   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 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 "client_priv.h"
#include "my_default.h"
#include "mysqlcheck.h"
#include <m_ctype.h>
#include <mysql_version.h>
#include <mysqld_error.h>

#include <string>
#include <vector>

using namespace Mysql::Tools::Check;

using std::string;
using std::vector;


/* ALTER instead of repair. */
#define MAX_ALTER_STR_SIZE 128 * 1024
#define KEY_PARTITIONING_CHANGED_STR "KEY () partitioning changed"

static MYSQL *sock= 0;
static my_bool opt_alldbs= 0, opt_check_only_changed= 0, opt_extended= 0,
               opt_databases= 0, opt_fast= 0,
               opt_medium_check = 0, opt_quick= 0, opt_all_in_1= 0,
               opt_silent= 0, opt_auto_repair= 0, ignore_errors= 0,
               opt_frm= 0, opt_fix_table_names= 0, opt_fix_db_names= 0, opt_upgrade= 0,
               opt_write_binlog= 1;
static uint verbose = 0;
static string opt_skip_database;
int what_to_do = 0;

void (*DBError)(MYSQL *mysql, string when);

static int first_error = 0;
vector<string> tables4repair, tables4rebuild, alter_table_cmds, failed_tables;


static int process_all_databases();
static int process_databases(vector<string> db_names);
static int process_selected_tables(string db, vector<string> table_names);
static int process_all_tables_in_db(string database);
static int process_one_db(string database);
static int use_db(string database);
static int handle_request_for_tables(string tables);
static void print_result();
static string escape_table_name(string src);


static int process_all_databases()
{
  MYSQL_ROW row;
  MYSQL_RES *tableres;
  int result = 0;

  if (mysql_query(sock, "SHOW DATABASES") ||
      !(tableres = mysql_store_result(sock)))
  {
    my_printf_error(0, "Error: Couldn't execute 'SHOW DATABASES': %s",
                    MYF(0), mysql_error(sock));
    return 1;
  }
  while ((row = mysql_fetch_row(tableres)))
  {
    if (process_one_db(row[0]))
      result = 1;
  }
  mysql_free_result(tableres);
  return result;
}
/* process_all_databases */


static int process_databases(vector<string> db_names)
{
  int result = 0;
  vector<string>::iterator it;
  for (it= db_names.begin() ; it != db_names.end(); it++)
  {
    if (process_one_db(*it))
      result = 1;
  }
  return result;
} /* process_databases */


static int process_selected_tables(string db, vector<string> table_names)
{
  if (use_db(db))
    return 1;
  vector<string>::iterator it;

  for (it= table_names.begin(); it != table_names.end(); it++)
  {
    if (what_to_do != DO_UPGRADE)
      *it= escape_table_name(*it);
    handle_request_for_tables(*it);
  }
  return 0;
} /* process_selected_tables */


static inline void escape_str(string src, size_t start, size_t end, string &res)
{
  res+= '`';
  for (size_t i= start; i < end; i++)
  {
    switch (src[i]) {
    case '`':            /* Escape backtick character. */
      res+= '`';
      /* Fall through. */
    default:
      res+= src[i];
    }
  }
  res+= '`';
}


static string escape_table_name(string src)
{
  string res= "";

  escape_str(src, 0, src.length(), res);
  return res;
}


static string escape_db_table_name(string src, size_t dot_pos)
{
  string res= "";

  /* Escape database name. */
  escape_str(src, 0, dot_pos - 1, res);
  /* Add a dot. */
  res+= '.';
  /* Escape table name. */
  escape_str(src, dot_pos, src.length(), res);

  return res;
}

static int process_all_tables_in_db(string database)
{
  MYSQL_RES *res= NULL;
  MYSQL_ROW row;
  uint num_columns;

  if (use_db(database))
    return 1;
  if ((mysql_query(sock, "SHOW /*!50002 FULL*/ TABLES") &&
       mysql_query(sock, "SHOW TABLES")) ||
      !(res= mysql_store_result(sock)))
  {
    my_printf_error(0, "Error: Couldn't get table list for database %s: %s",
                    MYF(0), database.c_str(), mysql_error(sock));
    return 1;
  }

  num_columns= mysql_num_fields(res);

  vector<string> table_names;

  while ((row = mysql_fetch_row(res)))
  {
    /* Skip views if we don't perform renaming. */
    if ((what_to_do != DO_UPGRADE) && (num_columns == 2) && (strcmp(row[1], "VIEW") == 0))
      continue;

    table_names.push_back(row[0]);
  }
  mysql_free_result(res);

  process_selected_tables(database, table_names);
  return 0;
} /* process_all_tables_in_db */


static int run_query(string query)
{
  if (mysql_query(sock, query.c_str()))
  {
    fprintf(stderr, "Failed to run query \"%s\"\n", query.c_str());
    fprintf(stderr, "Error: %s\n", mysql_error(sock));
    return 1;
  }
  return 0;
}


static int fix_table_storage_name(string name)
{
  if (strncmp(name.c_str(), "#mysql50#", 9))
    return 1;
  int rc= run_query("RENAME TABLE `" + name + "` TO `" + name.substr(9) + "`");
  if (verbose)
    printf("%-50s %s\n", name.c_str(), rc ? "FAILED" : "OK");
  return rc;
}

static int fix_database_storage_name(string name)
{
  if (strncmp(name.c_str(), "#mysql50#", 9))
    return 1;
  int rc= run_query("ALTER DATABASE `" + name + "` UPGRADE DATA DIRECTORY NAME");
  if (verbose)
    printf("%-50s %s\n", name.c_str(), rc ? "FAILED" : "OK");
  return rc;
}

static int rebuild_table(string name)
{
  int rc= 0;
  string query= "ALTER TABLE " + name + " FORCE";
  if (mysql_real_query(sock, query.c_str(), (uint)query.length()))
  {
    fprintf(stderr, "Failed to %s\n", query.c_str());
    fprintf(stderr, "Error: %s\n", mysql_error(sock));
    rc= 1;
  }
  else
    printf("%s\nRunning  : %s\nstatus   : OK\n", name.c_str(), query.c_str());
  return rc;
}

static int process_one_db(string database)
{
  if (opt_skip_database.length() > 0 && opt_alldbs
    && database == opt_skip_database)
    return 0;

  if (what_to_do == DO_UPGRADE)
  {
    int rc= 0;
    if (opt_fix_db_names && !strncmp(database.c_str(),"#mysql50#", 9))
    {
      rc= fix_database_storage_name(database);
      database= database.substr(9);
    }
    if (rc || !opt_fix_table_names)
      return rc;
  }
  return process_all_tables_in_db(database);
}


static int use_db(string database)
{
  if (mysql_get_server_version(sock) >= FIRST_INFORMATION_SCHEMA_VERSION &&
      !my_strcasecmp(
        &my_charset_latin1, database.c_str(), INFORMATION_SCHEMA_DB_NAME))
    return 1;
  if (mysql_get_server_version(sock) >= FIRST_PERFORMANCE_SCHEMA_VERSION &&
      !my_strcasecmp(
        &my_charset_latin1, database.c_str(), PERFORMANCE_SCHEMA_DB_NAME))
    return 1;
  if (mysql_select_db(sock, database.c_str()))
  {
    DBError(sock, "when selecting the database");
    return 1;
  }
  return 0;
} /* use_db */

static int disable_binlog()
{
  return run_query("SET SQL_LOG_BIN=0");
}

static int handle_request_for_tables(string tables)
{
  string operation, options;

  switch (what_to_do) {
  case DO_CHECK:
    operation = "CHECK";
    if (opt_quick)              options+= " QUICK";
    if (opt_fast)               options+= " FAST";
    if (opt_medium_check)       options+= " MEDIUM"; /* Default */
    if (opt_extended)           options+= " EXTENDED";
    if (opt_check_only_changed) options+= " CHANGED";
    if (opt_upgrade)            options+= " FOR UPGRADE";
    break;
  case DO_REPAIR:
    operation= (opt_write_binlog) ? "REPAIR" : "REPAIR NO_WRITE_TO_BINLOG";
    if (opt_quick)              options+= " QUICK";
    if (opt_extended)           options+= " EXTENDED";
    if (opt_frm)                options+= " USE_FRM";
    break;
  case DO_ANALYZE:
    operation= (opt_write_binlog) ? "ANALYZE" : "ANALYZE NO_WRITE_TO_BINLOG";
    break;
  case DO_OPTIMIZE:
    operation= (opt_write_binlog) ? "OPTIMIZE" : "OPTIMIZE NO_WRITE_TO_BINLOG";
    break;
  case DO_UPGRADE:
    return fix_table_storage_name(tables);
  }

  string query= operation + " TABLE " + tables + " " + options;

  if (mysql_real_query(sock, query.c_str(), query.length()))
  {
    DBError(sock,
      "when executing '" + operation + " TABLE ... " + options + "'");
    return 1;
  }
  print_result();
  return 0;
}


static void print_result()
{
  MYSQL_RES *res;
  MYSQL_ROW row;
  char prev[NAME_LEN*3+2];
  char prev_alter[MAX_ALTER_STR_SIZE];
  uint i;
  size_t dot_pos;
  my_bool found_error=0, table_rebuild=0;

  res = mysql_use_result(sock);
  dot_pos= strlen(sock->db) + 1;

  prev[0] = '\0';
  prev_alter[0]= 0;
  for (i = 0; (row = mysql_fetch_row(res)); i++)
  {
    int changed = strcmp(prev, row[0]);
    my_bool status = !strcmp(row[2], "status");

    if (status)
    {
      /*
        if there was an error with the table, we have --auto-repair set,
        and this isn't a repair op, then add the table to the tables4repair
        list
      */
      if (found_error && opt_auto_repair && what_to_do != DO_REPAIR &&
          strcmp(row[3],"OK"))
      {
        if (table_rebuild)
        {
          if (prev_alter[0])
            alter_table_cmds.push_back(prev_alter);
          else
            tables4rebuild.push_back(escape_db_table_name(prev, dot_pos));
        }
        else
        {
          tables4repair.push_back(escape_db_table_name(prev, dot_pos));
        }

      }
      found_error=   0;
      table_rebuild= 0;
      prev_alter[0]= '\0';
      if (opt_silent)
        continue;
    }
    if (status && changed)
      printf("%-50s %s", row[0], row[3]);
    else if (!status && changed)
    {
      printf("%s\n%-9s: %s", row[0], row[2], row[3]);
      if (opt_auto_repair && strcmp(row[2],"note"))
      {
        const char *alter_txt= strstr(row[3], "ALTER TABLE");
        found_error= 1;
        if (alter_txt)
        {
          table_rebuild= 1;
          const char *match_str;
          if (!strncmp(row[3], KEY_PARTITIONING_CHANGED_STR,
                       strlen(KEY_PARTITIONING_CHANGED_STR)))
          {
            if (strstr(alter_txt, "PARTITION BY") &&
                strlen(alter_txt) < MAX_ALTER_STR_SIZE)
            {
              strcpy(prev_alter, alter_txt);
            }
            else
            {
               printf("\nError: Alter command unknown or too long (%d >= %d), "
                      "please investigate the above or dump/reload to fix it!"
                      "\n",
                      (int)strlen(alter_txt),
                      MAX_ALTER_STR_SIZE);
              found_error=   0;
              table_rebuild= 0;
              prev_alter[0]= '\0';
              failed_tables.push_back(row[0]);
            }
          }
          else if ((match_str= strstr(alter_txt, "` UPGRADE PARTITIONING")) &&
                   strlen(match_str) == 22)
          {
            strcpy(prev_alter, alter_txt);
          }
        }
      }
    }
    else
      printf("%-9s: %s", row[2], row[3]);
    my_stpcpy(prev, row[0]);
    putchar('\n');
  }
  /* add the last table to be repaired to the list */
  if (found_error && opt_auto_repair && what_to_do != DO_REPAIR)
  {
    if (table_rebuild)
    {
      if (prev_alter[0])
        alter_table_cmds.push_back(prev_alter);
      else
        tables4rebuild.push_back(escape_db_table_name(prev, dot_pos));
    }
    else
    {
      tables4repair.push_back(escape_db_table_name(prev, dot_pos));
    }
  }
  mysql_free_result(res);
}

void Mysql::Tools::Check::mysql_check(MYSQL* connection, int what_to_do,
                my_bool opt_alldbs,
                my_bool opt_check_only_changed, my_bool opt_extended,
                my_bool opt_databases, my_bool opt_fast,
                my_bool opt_medium_check, my_bool opt_quick,
                my_bool opt_all_in_1, my_bool opt_silent,
                my_bool opt_auto_repair, my_bool ignore_errors,
                my_bool opt_frm, my_bool opt_fix_table_names,
                my_bool opt_fix_db_names, my_bool opt_upgrade,
                my_bool opt_write_binlog, uint verbose,
                string opt_skip_database, vector<string> arguments,
                void (*dberror)(MYSQL *mysql, string when))
{
  ::sock= connection;
  ::what_to_do= what_to_do;
  ::opt_alldbs= opt_alldbs;
  ::opt_check_only_changed= opt_check_only_changed;
  ::opt_extended= opt_extended;
  ::opt_databases= opt_databases;
  ::opt_fast= opt_fast;
  ::opt_medium_check= opt_medium_check;
  ::opt_quick= opt_quick;
  ::opt_all_in_1= opt_all_in_1;
  ::opt_silent= opt_silent;
  ::opt_auto_repair= opt_auto_repair;
  ::ignore_errors= ignore_errors;
  ::opt_frm= opt_frm;
  ::opt_fix_table_names= opt_fix_table_names;
  ::opt_fix_db_names= opt_fix_db_names;
  ::opt_upgrade= opt_upgrade;
  ::opt_write_binlog= opt_write_binlog;
  ::verbose= verbose;
  ::opt_skip_database= opt_skip_database;
  ::DBError= dberror;

  if (!::opt_write_binlog)
  {
    if (disable_binlog()) {
      first_error= 1;
      return;
    }
  }

  if (::opt_alldbs)
    process_all_databases();
  /* Only one database and selected table(s) */
  else if (arguments.size() > 1 && !::opt_databases)
  {
    string db_name= arguments[0];
    arguments.erase(arguments.begin());
    process_selected_tables(db_name, arguments);
  }
  /* One or more databases, all tables */
  else
    process_databases(arguments);
  if (::opt_auto_repair)
  {
    if (!::opt_silent)
    {
      if (!(tables4repair.empty() && tables4rebuild.empty()))
        puts("\nRepairing tables");

      if (!(alter_table_cmds.empty()))
        puts("\nUpgrading tables");
    }

    ::what_to_do = DO_REPAIR;

    vector<string>::iterator it;
    for (it = tables4repair.begin(); it != tables4repair.end() ; it++)
    {
      handle_request_for_tables(*it);
    }
    for (it = tables4rebuild.begin(); it != tables4rebuild.end() ; it++)
    {
      rebuild_table(*it);
    }
    for (it = alter_table_cmds.begin(); it != alter_table_cmds.end() ; it++)
    {
      if (0 == run_query(*it))
        printf("Running  : %s\nstatus   : OK\n", (*it).c_str());
      else
      {
        fprintf(stderr, "Failed to %s\n", (*it).c_str());
        fprintf(stderr, "Error: %s\n", mysql_error(sock));
      }
    }
    if (!failed_tables.empty())
    {
      fprintf(stderr,
              "These tables cannot be automatically upgraded,"
              " see the log above:\n");
    }
    for (it = failed_tables.begin(); it != failed_tables.end() ; it++)
    {
      fprintf(stderr, "%s\n", it->c_str());
    }
  }
}

Program::Program()
  : m_what_to_do(0),
  m_auto_repair(false),
  m_upgrade(false),
  m_verbose(false),
  m_ignore_errors(false),
  m_write_binlog(false),
  m_process_all_dbs(false),
  m_fix_table_names(false),
  m_fix_db_names(false),
  m_connection(NULL),
  m_error_callback(NULL)
{
}

int Program::check_databases(MYSQL* connection, vector<string> databases)
{
  this->m_connection= connection;
  this->m_process_all_dbs= false;
  return this->set_what_to_do(DO_CHECK)
    ->execute(databases);
}

int Program::check_all_databases(MYSQL* connection)
{
  this->m_connection= connection;
  this->m_process_all_dbs= true;
  return this->set_what_to_do(DO_CHECK)
    ->execute(vector<string>());
}

int Program::upgrade_databases(MYSQL* connection, vector<string> databases)
{
  this->m_connection= connection;
  this->m_process_all_dbs= false;
  return this->set_what_to_do(DO_UPGRADE)
    ->execute(databases);
}

int Program::upgrade_all_databases(MYSQL* connection)
{
  this->m_connection= connection;
  this->m_process_all_dbs= true;
  return this->set_what_to_do(DO_UPGRADE)
    ->execute(vector<string>());
}

Program* Program::enable_auto_repair(bool enable)
{
  this->m_auto_repair= enable;
  return this;
}

Program* Program::enable_upgrade(bool enable)
{
  this->m_upgrade= enable;
  return this;
}

Program* Program::enable_verbosity(bool enable)
{
  this->m_verbose= enable;
  return this;
}

Program* Program::enable_writing_binlog(bool enable)
{
  this->m_write_binlog= enable;
  return this;
}

Program* Program::enable_fixing_table_names(bool enable)
{
  this->m_fix_table_names= enable;
  return this;
}

Program* Program::enable_fixing_db_names(bool enable)
{
  this->m_fix_db_names= enable;
  return this;
}

Program* Program::set_ignore_errors(bool ignore)
{
  this->m_ignore_errors= ignore;
  return this;
}

Program* Program::set_skip_database(string database)
{
  this->m_database_to_skip= database;
  return this;
}

Program* Program::set_error_callback(void (
  *error_callback)(MYSQL *mysql, string when))
{
  this->m_error_callback= error_callback;
  return this;
}

Program* Program::set_what_to_do(int functionality)
{
  this->m_what_to_do= functionality;
  return this;
}

int Program::execute(vector<string> positional_options)
{
  Mysql::Tools::Check::mysql_check(
    this->m_connection, // connection
    this->m_what_to_do, // what_to_do
    this->m_process_all_dbs, // opt_alldbs
    false, // opt_check_only_changed
    false, // opt_extended
    !this->m_process_all_dbs, // opt_databases
    false, // opt_fast
    false, // opt_medium_check
    false, // opt_quick
    false, // opt_all_in_1
    false, // opt_silent
    this->m_auto_repair, // opt_auto_repair
    this->m_ignore_errors, // ignore_errors
    false, // opt_frm
    this->m_fix_table_names, // opt_fix_table_names
    this->m_fix_db_names, // opt_fix_db_names
    this->m_upgrade, // opt_upgrade
    this->m_write_binlog, // opt_write_binlog
    this->m_verbose, // verbose
    this->m_database_to_skip,
    positional_options,
    this->m_error_callback);
  return 0;
}

