/*
  Copyright (c) 2015, 2016 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 "program.h"
#include "i_connection_provider.h"
#include "thread_specific_connection_provider.h"
#include "single_transaction_connection_provider.h"
#include "simple_id_generator.h"
#include "i_progress_watcher.h"
#include "standard_progress_watcher.h"
#include "i_crawler.h"
#include "mysql_crawler.h"
#include "i_chain_maker.h"
#include "mysqldump_tool_chain_maker.h"
#include <boost/chrono.hpp>

using namespace Mysql::Tools::Dump;

void Program::close_redirected_stderr()
{
  if (m_stderr != NULL)
    fclose(m_stderr);
}

void Program::error_log_file_callback(char*)
{
  if (!m_error_log_file.has_value())
    return;
  this->close_redirected_stderr();
  m_stderr= freopen(m_error_log_file.value().c_str(), "a", stderr);
  if (m_stderr == NULL)
  {
    this->error(Mysql::Tools::Base::Message_data(errno,
      "Cannot append error log to specified file: \""
      + m_error_log_file.value() + "\"",
      Mysql::Tools::Base::Message_type_error));
  }
}

bool Program::message_handler(const Mysql::Tools::Base::Message_data& message)
{
  this->error(message);
  return false;
}

void Program::error(const Mysql::Tools::Base::Message_data& message)
{
  std::cerr << this->get_name() << ": [" << message.get_message_type_string()
    << "] (" << message.get_code() << ") " << message.get_message()
    << std::endl;

  if (message.get_message_type() == Mysql::Tools::Base::Message_type_error)
  {
    std::cerr << "Dump process encountered error and will not continue."
      << std::endl;
    m_error_code.store((int)message.get_code());
  }
}

void Program::create_options()
{
  this->create_new_option(&m_error_log_file, "log-error-file",
    "Append warnings and errors to specified file.")
    ->add_callback(new Mysql::Instance_callback<void, char*, Program>(
    this, &Program::error_log_file_callback));
  this->create_new_option(&m_watch_progress, "watch-progress",
    "Shows periodically dump process progress information on error output. "
    "Progress information include both completed and total number of "
    "tables, rows and other objects collected.")
    ->set_value(true);
  this->create_new_option(&m_single_transaction, "single-transaction",
    "Creates a consistent snapshot by dumping all tables in a single "
    "transaction. Works ONLY for tables stored in storage engines which "
    "support multiversioning (currently only InnoDB does); the dump is NOT "
    "guaranteed to be consistent for other storage engines. "
    "While a --single-transaction dump is in process, to ensure a valid "
    "dump file (correct table contents and binary log position), no other "
    "connection should use the following statements: ALTER TABLE, DROP "
    "TABLE, RENAME TABLE, TRUNCATE TABLE, as consistent snapshot is not "
    "isolated from them. This option is mutually exclusive with "
    "--add-locks option.");
}

void  Program::check_mutually_exclusive_options()
{
  /*
    In case of --add-locks we dont allow parallelism
  */
  if (m_mysqldump_tool_chain_maker_options->m_default_parallelism ||
     m_mysqldump_tool_chain_maker_options->get_parallel_schemas_thread_count())
  {
    if (m_mysqldump_tool_chain_maker_options->m_formatter_options->m_add_locks)
      m_mysql_chain_element_options->get_program()->error(
        Mysql::Tools::Base::Message_data(1, "Usage of --add-locks "
        "is mutually exclusive with parallelism.",
        Mysql::Tools::Base::Message_type_error));
  }
}

int Program::get_total_connections()
{
  /*
    total thread count for mysqlpump would be as below:
     1 main thread +
     default queues thread (specified by default parallelism) +
     total parallel-schemas without threads specified * dp +
     total threads mentioned in parallel-schemas
  */
 
   int dp= m_mysqldump_tool_chain_maker_options->m_default_parallelism;
  return (1 + dp +
    m_mysqldump_tool_chain_maker_options->get_parallel_schemas_thread_count() +
    (m_mysqldump_tool_chain_maker_options->
     get_parallel_schemas_with_default_thread_count() * dp));
}

int Program::get_error_code()
{
  return m_error_code.load();
}

int Program::execute(std::vector<std::string> positional_options)
{
  I_connection_provider* connection_provider= NULL;
  int num_connections= get_total_connections();

  Mysql::I_callable<bool, const Mysql::Tools::Base::Message_data&>*
    message_handler= new Mysql::Instance_callback
    <bool, const Mysql::Tools::Base::Message_data&, Program>(
    this, &Program::message_handler);

  try
  {
    connection_provider=
      m_single_transaction ?
      new Single_transaction_connection_provider(this, num_connections, message_handler)
      : new Thread_specific_connection_provider(this);
  }
  catch (std::exception e)
  {
    this->error(Mysql::Tools::Base::Message_data(
      0, "Error during creating connection.",
      Mysql::Tools::Base::Message_type_error));
  }

  Mysql::Tools::Base::Mysql_query_runner* runner= connection_provider
         ->get_runner(message_handler);
  if (mysql_get_server_version(runner->get_low_level_connection()) < 50708)
  {
    std::cerr << "Server version is not compatible. Server version should "
                 "be 5.7.8 or above.";
    delete runner;
    delete message_handler;
    delete connection_provider;
    return 0;
  }

  Simple_id_generator* id_generator= new Simple_id_generator();

  boost::chrono::high_resolution_clock::time_point start_time=
    boost::chrono::high_resolution_clock::now();

  I_progress_watcher* progress_watcher= NULL;

  if (m_watch_progress)
  {
    progress_watcher= new Standard_progress_watcher(
      message_handler, id_generator);
  }
  I_crawler* crawler= new Mysql_crawler(
    connection_provider, message_handler, id_generator,
    m_mysql_chain_element_options, this);
  m_mysqldump_tool_chain_maker_options->process_positional_options(
    positional_options);
  check_mutually_exclusive_options();
  I_chain_maker* chain_maker= new Mysqldump_tool_chain_maker(
    connection_provider, message_handler, id_generator,
    m_mysqldump_tool_chain_maker_options, this);

  crawler->register_chain_maker(chain_maker);
  if (progress_watcher != NULL)
  {
    crawler->register_progress_watcher(progress_watcher);
    chain_maker->register_progress_watcher(progress_watcher);
  }

  crawler->enumerate_objects();

  delete runner;
  delete crawler;
  if (progress_watcher != NULL)
    delete progress_watcher;
  delete id_generator;
  delete connection_provider;
  delete message_handler;
  delete chain_maker;

  if (!get_error_code())
  {
    std::cerr << "Dump completed in " <<
      boost::chrono::duration_cast<boost::chrono::milliseconds>(
      boost::chrono::high_resolution_clock::now() - start_time) << std::endl;
  }
  return get_error_code();
}

std::string Program::get_description()
{
  return "MySQL utility for dumping data from databases to external file.";
}

int Program::get_first_release_year()
{
  return 2014;
}

std::string Program::get_version()
{
  return "1.0.0";
}

Program::~Program()
{
  delete m_mysql_chain_element_options;
  delete m_mysqldump_tool_chain_maker_options;
  this->close_redirected_stderr();
}

void Program::short_usage()
{
  std::cout << "Usage: " << get_name() <<" [OPTIONS] [--all-databases]"
            << std::endl;
  std::cout << "OR     " << get_name() <<" [OPTIONS] --databases DB1 [DB2 DB3...]"
            << std::endl;
  std::cout << "OR     " << get_name() <<" [OPTIONS] database [tables]"
            << std::endl;
}

Program::Program()
  : Abstract_connection_program(),
  m_stderr(NULL),
  m_error_code(0)
{
  m_mysql_chain_element_options= new Mysql_chain_element_options(this);
  m_mysqldump_tool_chain_maker_options=
    new Mysqldump_tool_chain_maker_options(m_mysql_chain_element_options);

  this->add_provider(m_mysql_chain_element_options);
  this->add_provider(m_mysqldump_tool_chain_maker_options);
}

const char *load_default_groups[]=
{
  "client", /* Read settings how to connect to server. */
  "mysql_dump", /* Read special settings for mysql_dump. */
  0
};

static Program program;

int main(int argc, char **argv)
{
  ::program.run(argc, argv);
  return 0;
}
