#include <my_global.h>
#include <m_string.h>
extern "C" {
#include "maria_def.h"
}
#undef LSN_MAX
#include "aria_backup_client.h"
#include "backup_copy.h"
#include "common.h"
#include "sql_table.h"
#include "ma_checkpoint.h"
#include "ma_recovery.h"
#include "backup_debug.h"
#include "aria_backup.h"
#include <thread>
#include <string>
#include <vector>
#include <memory>
#include <limits>
#include <unordered_map>
#include <atomic>
#include <utility>
#include <sstream>
#include <iomanip>
#include <cstdlib>

namespace aria {

const char *log_preffix = "aria_log.";


static std::string log_file_name_only(size_t log_num) {
	std::string log_file;
	{
		std::stringstream ss;
		ss << std::setw(8) << std::setfill('0') << log_num;
		log_file.append(log_preffix).append(ss.str());
	}
	return log_file;
}


static std::string log_file_name(const char *datadir_path, size_t log_num) {
	std::string log_file(datadir_path);
	return log_file.append("/").append(log_file_name_only(log_num));
}


class LogFileCollection
{
  uint32 m_first;
  uint32 m_count;
public:
  uint32 first() const { return m_first; }
  uint32 count() const { return m_count; }
  uint32 last() const
  {
    DBUG_ASSERT(m_count > 0);
    return m_first + m_count - 1;
  }

  // Initialize by checking existing log files on the disk
  LogFileCollection(const char *datadir, uint32 max_log_no)
  {
    uint32 end= find_greatest_existing_log(datadir, max_log_no);
    if (!end)
    {
      // No log files were found at all
      m_first= 0;
      m_count= 0;
    }
    else if (end == 1)
    {
      // Just the very first one log file (aria_log.00000001) was found.
      m_first= 1;
      m_count= 1;
    }
    else
    {
      // Multiple files were found
      m_first= find_greatest_missing_log(datadir, end - 1) + 1;
      m_count= 1 + end - m_first;
    }
  }

  /*
    Skip all missing log files and find the greatest existing log file, or
    Skip all existing log files and find the greatest missing log file.

    @param datadir  - Search files in this directory
    @param start    - Start searching from this log number and go downto 1.
    @param kind     - true  - search for an existing file
                      false - search for a missing file.
    @returns        - [1..start] - the greatest found log file
                                   of the searched kind
                    - 0  - if no log files of this kind
                           were found in the range [1..start].
  */
  static uint32 find_greatest_existing_or_missing_log(const char *datadir,
                                                      uint32 start,
                                                      bool kind)
  {
    DBUG_ASSERT(start > 0);
    for (uint32 i= start; i > 0; i--)
    {
      if (file_exists(log_file_name(datadir, i).c_str()) == kind)
        return i;
    }
    return 0; // No log files of the searched kind were found
  }

  static uint32 find_greatest_existing_log(const char *datadir, uint32 start)
  {
    return find_greatest_existing_or_missing_log(datadir, start, true);
  }

  static uint32 find_greatest_missing_log(const char *datadir, uint32 start)
  {
    return find_greatest_existing_or_missing_log(datadir, start, false);
  }

  /*
    In some scenarios (e.g. log rotate) some new log files can appear
    outside of the initially assumed [first,last] log number range.
    This function adds all extra files behind "last".
  */
  void find_logs_after_last(const char *datadir)
  {
    DBUG_ASSERT(m_count > 0);
    for ( ;
         file_exists(log_file_name(datadir, last() + 1).c_str()) ;
         m_count++)
    {  }
  }

  void report_found(unsigned thread_num) const
  {
    if (m_count)
      msg(thread_num,
          "Found %u aria log files, "
          "minimum log number %u, "
          "maximum log number %u",
          m_count, m_first, last());
  }

  void die_if_missing(uint32 logno) const
  {
    DBUG_ASSERT(logno > 0);
    if (!m_count || m_first > logno || last() < logno)
      die("Aria log file %u does not exists.", logno);
  }
};


class Table {
public:
	struct Partition {
		std::string m_file_path;
		File m_index_file = -1;
		MY_STAT m_index_file_stat;
		File m_data_file = -1;
		MY_STAT m_data_file_stat;
	};
	Table() = default;
	Table (Table &&other) = delete;
	Table &  operator= (Table &&other) = delete;
	Table(const Table &) = delete;
	Table & operator= (const Table &) = delete;
	~Table();
	bool init(const char *data_file_path);
	bool open(MYSQL *con, bool opt_no_lock, unsigned thread_num);
	bool close();
	bool copy(ds_ctxt_t *ds, unsigned thread_num);

	bool is_online_backup_safe() const {
		DBUG_ASSERT(is_opened());
		return m_cap.online_backup_safe;
	}
	bool is_stats() const {
		return is_stats_table(m_db.c_str(), m_table.c_str());
	}
	bool is_log() const {
		return is_log_table(m_db.c_str(), m_table.c_str());
	}
	bool is_opened() const {
		return !m_partitions.empty() &&
			m_partitions[0].m_index_file >= 0 && m_partitions[0].m_data_file >= 0;
	};
	std::string &get_full_name() {
		return m_full_name;
	}
	std::string &get_db() { return m_db; }
	std::string &get_table() { return m_table; }
	std::string &get_version() { return m_table_version; }
	bool is_partitioned() const { return m_partitioned; }
	void add_partition(const Table &partition) {
		DBUG_ASSERT(is_partitioned());
		m_partitions.push_back(partition.m_partitions[0]);
	}
#ifndef DBUG_OFF
	const std::string& get_sql_name() const { return m_sql_name; }
#endif //DBUG_OFF
private:

	bool copy(ds_ctxt_t *ds, bool is_index, unsigned thread_num);
	// frm and par files will be copied under BLOCK_DDL stage in
	// backup_copy_non_system()
	bool copy_frm_and_par(ds_ctxt_t *ds, unsigned thread_num);
	bool read_table_version_id(File file);

	std::string m_db;
	std::string m_table;
	std::string m_full_name;
	std::string m_frm_par_path;
	std::string m_table_version;
#ifndef DBUG_OFF
	std::string m_sql_name;
#endif //DBUG_OFF
	bool m_partitioned = false;
	std::vector<Partition> m_partitions;
	ARIA_TABLE_CAPABILITIES m_cap;
};

Table::~Table() {
	(void)close();
}

bool Table::init(const char *data_file_path) {
		DBUG_ASSERT(data_file_path);

		const char *ext_pos = strrchr(data_file_path, '.');
		if (!ext_pos)
			return false;

		char db_name_orig[FN_REFLEN];
		char table_name_orig[FN_REFLEN];
		parse_db_table_from_file_path(
			data_file_path, db_name_orig, table_name_orig);
		if (!db_name_orig[0] || !table_name_orig[0])
			return false;
		char db_name_conv[FN_REFLEN];
		char table_name_conv[FN_REFLEN];
		filename_to_tablename(db_name_orig, db_name_conv, sizeof(db_name_conv));
		filename_to_tablename(
			table_name_orig, table_name_conv, sizeof(table_name_conv));
		if (!db_name_conv[0] || !table_name_conv[0])
			return false;

		if (strstr(data_file_path, "#P#"))
			m_partitioned = true;

		const char *table_name_begin = strrchr(data_file_path, FN_LIBCHAR);
		if (!table_name_begin)
			return false;
		m_frm_par_path.assign(data_file_path, table_name_begin + 1).
			append(table_name_orig);

		m_db.assign(db_name_conv);
		m_table.assign(table_name_conv);
		// TODO: find the correct way to represent quoted table/db names
		m_full_name.assign("`").append(m_db).append("`.`").
			append(m_table).append("`");
#ifndef DBUG_OFF
		m_sql_name.assign(m_db).append("/").append(m_table);
#endif // DBUG_OFF
		Partition partition;
		partition.m_file_path.assign(data_file_path, ext_pos - data_file_path);
		m_partitions.push_back(std::move(partition));
		return true;
}

bool Table::read_table_version_id(File file) {
	m_table_version = ::read_table_version_id(file);
	return m_table_version.empty();
}

bool Table::open(MYSQL *con, bool opt_no_lock, unsigned thread_num) {
	int error= 1;
	bool have_capabilities = false;
	File frm_file = -1;

	if (!opt_no_lock && !backup_lock(con, m_full_name.c_str())) {
		msg(thread_num, "Error on BACKUP LOCK for aria table %s",
			m_full_name.c_str());
		goto exit;
	}

	for (Partition &partition : m_partitions) {
		std::string file_path = partition.m_file_path + ".MAI";
		if ((partition.m_index_file= my_open(file_path.c_str(),
						O_RDONLY | O_SHARE | O_NOFOLLOW | O_CLOEXEC,
						MYF(MY_WME))) < 0) {
			msg(thread_num, "Error on aria table file open %s", file_path.c_str());
			goto exit;
		}
		if (!my_stat(file_path.c_str(), &partition.m_index_file_stat, MYF(0))) {
			msg(thread_num, "Error on aria table file stat %s", file_path.c_str());
			goto exit;
		}
		if (!have_capabilities) {
			if ((error= aria_get_capabilities(partition.m_index_file, &m_cap))) {
				msg(thread_num, "aria_get_capabilities failed: %d", error);
				goto exit;
			}
			have_capabilities = true;
		}

		file_path = partition.m_file_path + ".MAD";
		if ((partition.m_data_file= my_open(file_path.c_str(),
						O_RDONLY | O_SHARE | O_NOFOLLOW | O_CLOEXEC, MYF(MY_WME))) < 0) {
			msg(thread_num, "Error on aria table file open %s", file_path.c_str());
			goto exit;
		}
		if (!my_stat(file_path.c_str(), &partition.m_data_file_stat, MYF(0))) {
			msg(thread_num, "Error on aria table file stat %s", file_path.c_str());
			goto exit;
		}
	}

	if ((frm_file = mysql_file_open(
		key_file_frm, (m_frm_par_path + ".frm").c_str(),
		O_RDONLY | O_SHARE, MYF(0))) < 0) {
		msg(thread_num, "Error on aria table %s file open",
			(m_frm_par_path + ".frm").c_str());
		goto exit;
	}

	error = 0;

exit:
	if (!opt_no_lock && !backup_unlock(con)) {
		msg(thread_num, "Error on BACKUP UNLOCK for aria table %s",
			m_full_name.c_str());
		error = 1;
	}
	if (error)
		(void)close();
	else {
		(void)read_table_version_id(frm_file);
		mysql_file_close(frm_file, MYF(MY_WME));
	}
	return !error;
}

bool Table::close() {
	for (Partition &partition : m_partitions) {
		if (partition.m_index_file >= 0) {
			my_close(partition.m_index_file, MYF(MY_WME));
			partition.m_index_file = -1;
		}
		if (partition.m_data_file >= 0) {
			my_close(partition.m_data_file, MYF(MY_WME));
			partition.m_data_file = -1;
		}
	}
	return true;
}

bool Table::copy(ds_ctxt_t *ds, unsigned thread_num) {
	DBUG_ASSERT(is_opened());
	DBUG_MARIABACKUP_EVENT_LOCK("before_aria_table_copy",
          fil_space_t::name_type(m_sql_name.data(), m_sql_name.size()));
	bool result =
//		copy_frm_and_par(ds, thread_num) &&
			copy(ds, true, thread_num) && copy(ds, false, thread_num);
	return result;
}

bool Table::copy(ds_ctxt_t *ds, bool is_index, unsigned thread_num) {
	DBUG_ASSERT(ds);
	const char *ext = is_index ? ".MAI" : ".MAD";
	int error= 1;
	for (const Partition &partition : m_partitions) {
		ds_file_t		*dst_file = nullptr;
		uchar *copy_buffer = nullptr;
		std::string full_name = partition.m_file_path + ext;
		const char	*dst_path =
			(xtrabackup_copy_back || xtrabackup_move_back) ?
			full_name.c_str() : trim_dotslash(full_name.c_str());

		dst_file = ds_open(ds, dst_path,
			is_index ? &partition.m_index_file_stat : &partition.m_data_file_stat);
		if (!dst_file) {
			msg(thread_num, "error: cannot open the destination stream for %s",
				dst_path);
			goto err;
		}

		copy_buffer =
			reinterpret_cast<uchar *>(my_malloc(PSI_NOT_INSTRUMENTED,
			m_cap.block_size, MYF(0)));

		DBUG_MARIABACKUP_EVENT_LOCK(
			is_index ?
				"before_aria_index_file_copy":
				"before_aria_data_file_copy",
			fil_space_t::name_type(m_sql_name.data(),
                        m_sql_name.size()));

		for (ulonglong block= 0 ; ; block++) {
			size_t length = m_cap.block_size;
			if (is_index) {
				if ((error= aria_read_index(
							partition.m_index_file, &m_cap, block, copy_buffer) ==
							HA_ERR_END_OF_FILE))
					break;
			} else {
				if ((error= aria_read_data(
							partition.m_data_file, &m_cap, block, copy_buffer, &length) ==
							HA_ERR_END_OF_FILE))
					break;
			}
			if (error) {
				msg(thread_num, "error: aria_read %s failed:  %d",
					is_index ? "index" : "data", error);
				goto err;
			}
			xtrabackup_io_throttling();
			if ((error = ds_write(dst_file, copy_buffer, length))) {
				msg(thread_num, "error: aria_write failed:  %d", error);
				goto err;
			}
		}

		DBUG_MARIABACKUP_EVENT_LOCK(
			is_index ?
				"after_aria_index_file_copy":
				"after_aria_data_file_copy",
			fil_space_t::name_type(m_sql_name.data(),
                        m_sql_name.size()));

		error = 0;
		msg(thread_num, "aria table file %s is copied successfully.",
			full_name.c_str());

	err:
		if (dst_file)
			ds_close(dst_file);
		if (copy_buffer)
			my_free(copy_buffer);
		if (error)
			break;
	}
	return !error;
}

class BackupImpl {
public:
	BackupImpl(
		const char *datadir_path,
		const char *aria_log_path,
		ds_ctxt_t *datasink, bool opt_no_lock,
		std::vector<MYSQL *> &con_pool, ThreadPool &thread_pool) :
		m_datadir_path(datadir_path),
		m_aria_log_dir_path(aria_log_path),
		m_ds(datasink), m_con_pool(con_pool),
		m_tasks_group(thread_pool), m_thread_pool(thread_pool) { }
	~BackupImpl() { destroy(); }
	bool init();
	bool start(bool no_lock);
	bool wait_for_finish();
	bool copy_offline_tables(
		const std::unordered_set<table_key_t> *exclude_tables, bool no_lock,
		bool copy_stats);
	bool finalize();
	void set_post_copy_table_hook(const post_copy_table_hook_t &hook) {
		m_table_post_copy_hook = hook;
	}
	bool copy_log_tail() { return copy_log_tail(0, false); }
private:
	void destroy();
	void scan_job(bool no_lock, unsigned thread_num);
	bool copy_log_tail(unsigned thread_num, bool finalize);
	void copy_log_file_job(size_t log_num, unsigned thread_num);
	void destroy_log_tail();
	void process_table_job(Table *table, bool online_only, bool copy_stats,
		bool no_lock, unsigned thread_num);

	const char *m_datadir_path;
	const char *m_aria_log_dir_path;
	std::string aria_log_dir_path() const
	{
	  if (!m_aria_log_dir_path || !m_aria_log_dir_path[0])
	    return m_datadir_path;
          if (is_absolute_path(m_aria_log_dir_path))
            return m_aria_log_dir_path;
          return std::string(m_datadir_path).append("/")
                                            .append(m_aria_log_dir_path);
	}
	ds_ctxt_t *m_ds;
	std::vector<MYSQL *> &m_con_pool;

	TasksGroup m_tasks_group;

	std::mutex m_offline_tables_mutex;
	std::vector<std::unique_ptr<Table>> m_offline_tables;
	post_copy_table_hook_t m_table_post_copy_hook;

	ThreadPool &m_thread_pool;

	size_t m_last_log_num = 0;
	ds_file_t*	m_last_log_dst = nullptr;
	File m_last_log_src = -1;
};

bool BackupImpl::init() {
	DBUG_ASSERT(m_tasks_group.is_finished());
	return true;
};

void BackupImpl::destroy() {
	DBUG_ASSERT(m_tasks_group.is_finished());
	destroy_log_tail();
}

bool BackupImpl::start(bool no_lock) {
	DBUG_ASSERT(m_tasks_group.is_finished());
	m_tasks_group.push_task(
		std::bind(&BackupImpl::scan_job, this, no_lock, std::placeholders::_1));
	return true;
}

void BackupImpl::process_table_job(
	Table *table_ptr, bool online_only, bool copy_stats, bool no_lock,
	unsigned thread_num) {
	DBUG_ASSERT(table_ptr);
	DBUG_ASSERT(thread_num < m_con_pool.size());
	std::unique_ptr<Table> table(table_ptr);
	bool is_online;
	bool is_stats;
	bool need_copy;
	int result = 1;

	if (!m_tasks_group.get_result())
		goto exit;

	if (!table->open(m_con_pool[thread_num], no_lock, thread_num)) {
		// if table can't be opened, it might be removed or renamed, this is not
		// error for transactional tables
		table->close(); // Close opened table files
		goto exit;
	}

	is_online = table->is_online_backup_safe();
	is_stats = table->is_stats();

	need_copy = (!online_only || is_online) && (copy_stats || !is_stats);

	if (need_copy && !table->copy(m_ds, thread_num)) {
		table->close();
		DBUG_MARIABACKUP_EVENT_LOCK("after_aria_table_copy",
			fil_space_t::name_type(table->get_sql_name().data(),
                                               table->get_sql_name().size()));
		// if table is opened, it must be copied,
		// the corresponding diagnostic messages must be issued in Table::copy()
		result = 0;
		goto exit;
	}

	if (!table->close()) {
		msg(thread_num, "Can't close aria table %s.\n",
			table->get_full_name().c_str());
		result = 0;
		goto exit;
	}

	if (!need_copy) {
		std::lock_guard<std::mutex> lock(m_offline_tables_mutex);
		m_offline_tables.push_back(std::move(table));
	}
	else {
		DBUG_MARIABACKUP_EVENT_LOCK("after_aria_table_copy",
			fil_space_t::name_type(table->get_sql_name().data(),
                                               table->get_sql_name().size()));
		if (m_table_post_copy_hook)
			m_table_post_copy_hook(
				std::move(table->get_db()),
				std::move(table->get_table()),
				std::move(table->get_version()));
	}
exit:
	m_tasks_group.finish_task(result);
}


void BackupImpl::scan_job(bool no_lock, unsigned thread_num) {
	std::unordered_map<std::string, std::unique_ptr<Table>> partitioned_tables;

	std::string aria_log_dir_path_cache(aria_log_dir_path());
	std::string log_control_file_path(aria_log_dir_path_cache);
	log_control_file_path.append("/aria_log_control");
	if (!m_ds->copy_file(
		log_control_file_path.c_str(), "aria_log_control",
		0, false)) {
		msg("Aria log control file copying error.");
		m_tasks_group.finish_task(0);
		return;
	}

	msg(thread_num, "Loading aria_log_control.");
	aria_readonly= 1;
	maria_data_root= aria_log_dir_path_cache.c_str();
	if (ma_control_file_open(FALSE, FALSE, FALSE, O_RDONLY))
		die("Can't open Aria control file (%d)", errno);
	uint32 aria_log_control_last_log_number= last_logno;
	msg(thread_num, "aria_log_control: last_log_number: %d",
	    aria_log_control_last_log_number);
	ma_control_file_end();

	msg(thread_num, "Start scanning aria tables.");

	foreach_file_in_db_dirs(m_datadir_path, [&](const char *file_path)->bool {

		if (check_if_skip_table(file_path)) {
			msg(thread_num, "Skipping %s.", file_path);
			return true;
		}

		if (!ends_with(file_path, ".MAD"))
			return true;

		std::unique_ptr<Table> table(new Table());
		if (!table->init(file_path)) {
			msg(thread_num, "Can't init aria table %s.\n", file_path);
			return true;
		}

		if (table->is_log())
			return true;

		if (table->is_partitioned()) {
			auto table_it = partitioned_tables.find(table->get_full_name());
			if (table_it == partitioned_tables.end()) {
				partitioned_tables[table->get_full_name()] = std::move(table);
			} else {
				table_it->second->add_partition(*table);
			}
			return true;
		}

		m_tasks_group.push_task(
			std::bind(&BackupImpl::process_table_job, this, table.release(), true,
					false, no_lock, std::placeholders::_1));
		return true;
	});

	for (auto &table_it : partitioned_tables) {
		m_tasks_group.push_task(
			std::bind(&BackupImpl::process_table_job, this, table_it.second.release(),
				true, false, no_lock, std::placeholders::_1));
	}

	msg(thread_num, "Start scanning aria log files.");

        LogFileCollection logs(aria_log_dir_path_cache.c_str(),
                               aria_log_control_last_log_number);
        logs.report_found(thread_num);
        logs.die_if_missing(aria_log_control_last_log_number);

        m_last_log_num= logs.last();

        DBUG_MARIABACKUP_EVENT("after_scanning_log_files", {});

        for (uint32 i= logs.first(); i <= logs.last(); ++i)
          m_tasks_group.push_task(
            std::bind(&BackupImpl::copy_log_file_job, this,
                      i, std::placeholders::_1));

	msg(thread_num, "Stop scanning aria tables.");

	m_tasks_group.finish_task(1);
}

template<typename T>
T align_down(T n, ulint align_no)
{
	DBUG_ASSERT(align_no > 0);
	DBUG_ASSERT(ut_is_2pow(align_no));
	return n & ~(static_cast<T>(align_no) - 1);
}

static ssize_t copy_file_chunk(File src, ds_file_t* dst, size_t size) {
	size_t bytes_read;
	static const size_t max_buf_size = 10 * 1024 * 1024;
	size_t buf_size = size ? std::min(size, max_buf_size) : max_buf_size;
	std::unique_ptr<uchar[]> buf(new uchar[buf_size]);
	ssize_t copied_size = 0;
	bool unlim = !size;
	while((unlim || size) && (bytes_read = my_read(src, buf.get(),
		unlim ? buf_size : std::min(buf_size, size), MY_WME))) {
		if (bytes_read == size_t(-1))
			return -1;
		xtrabackup_io_throttling();
		if (ds_write(dst, buf.get(), bytes_read))
			return -1;
		copied_size += bytes_read;
		if (!unlim)
			size -= bytes_read;
	}
	return copied_size;
}

bool BackupImpl::copy_log_tail(unsigned thread_num, bool finalize) {
	bool result = false;
	std::string log_file = log_file_name(aria_log_dir_path().c_str(), m_last_log_num);
	std::string prev_log_file;
	ssize_t total_bytes_copied = 0;
	MY_STAT stat_info;
	my_off_t file_offset = 0;
	size_t to_copy_size = 0;

repeat:
	memset(&stat_info, 0, sizeof(MY_STAT));
	if (!m_tasks_group.get_result()) {
		msg(thread_num, "Skip copying aria lof file tail %s due to error.",
			log_file.c_str());
		result = true;
		goto exit;
	}

	msg(thread_num, "Start copying aria log file tail: %s", log_file.c_str());

	if (m_last_log_src < 0 && (m_last_log_src =
		my_open(log_file.c_str(), O_RDONLY | O_SHARE | O_NOFOLLOW | O_CLOEXEC,
			MYF(MY_WME))) < 0) {
		msg("Aria log file %s open failed: %d", log_file.c_str(), my_errno);
		goto exit;
	}

	if (!m_last_log_dst &&
		!(m_last_log_dst = ds_open(m_ds,
		                           log_file_name_only(m_last_log_num).c_str(),
		                           &stat_info, false))) {
		msg(thread_num, "error: failed to open the target stream for "
			"aria log file %s.",
			log_file.c_str());
		goto exit;
	}

// If there is no need to finalize log file copying, calculate the size to copy
// without the last page, which can be rewritten by the server
// (see translog_force_current_buffer_to_finish()).
	if (!finalize) {
		if (my_fstat(m_last_log_src, &stat_info, MYF(0))) {
			msg(thread_num, "error: failed to get file size for aria log file: %s.",
					log_file.c_str());
			goto exit;
		}
		if ((file_offset = my_tell(m_last_log_src, MYF(0))) == (my_off_t)(-1)) {
			msg(thread_num, "error: failed to get file offset for aria log file: %s.",
					log_file.c_str());
			goto exit;
		}
		DBUG_ASSERT(file_offset <= static_cast<my_off_t>(stat_info.st_size));
		to_copy_size = static_cast<size_t>(stat_info.st_size - file_offset);
		to_copy_size = to_copy_size >= TRANSLOG_PAGE_SIZE ?
			(align_down(to_copy_size, TRANSLOG_PAGE_SIZE) - TRANSLOG_PAGE_SIZE) : 0;
	}

// Copy from the last position to the end of file,
// excluding the last page is there is no need to finalize the copy.
	if ((to_copy_size || finalize) &&
		(total_bytes_copied = copy_file_chunk(m_last_log_src,
		m_last_log_dst, to_copy_size)) < 0) {
			msg(thread_num, "Aria log file %s chunk copy error", log_file.c_str());
			goto exit;
	}

	msg(thread_num, "Stop copying aria log file tail: %s, copied %zu bytes",
		log_file.c_str(), total_bytes_copied);

// Check if there is new log file, if yes, then copy the last page of the old
// one, and fix it last LSN in the log header, as it is changed on new
// log file creating by the server (see translog_create_new_file() and
// translog_max_lsn_to_header()). Then close the old log file and repeat
// the copying for the new log file.
	prev_log_file = std::move(log_file);
	log_file = log_file_name(aria_log_dir_path().c_str(), m_last_log_num + 1);
	if (file_exists(log_file.c_str())) {
		uchar lsn_buff[LSN_STORE_SIZE];
		msg(thread_num, "Found new aria log tail file: %s, start copy %s tail",
			log_file.c_str(), prev_log_file.c_str());
		if ((total_bytes_copied = copy_file_chunk(m_last_log_src,
			m_last_log_dst, 0)) < 0) {
				msg(thread_num, "Aria log file %s tail copy error",
					prev_log_file.c_str());
				goto exit;
		}

		if (my_pread(m_last_log_src, lsn_buff, LSN_STORE_SIZE,
			(LOG_HEADER_DATA_SIZE - LSN_STORE_SIZE), MYF(0)) < LSN_STORE_SIZE) {
			msg(thread_num, "Aria lsn store read error for log file %s",
				prev_log_file.c_str());
			goto exit;
		}

		if (ds_seek_set(m_last_log_dst, (LOG_HEADER_DATA_SIZE - LSN_STORE_SIZE))) {
			msg(thread_num, "Set aria log pointer error for log file %s",
				prev_log_file.c_str());
			goto exit;
		}

		if (ds_write(m_last_log_dst, lsn_buff, LSN_STORE_SIZE)) {
			msg(thread_num, "LSN write error for aria log file %s",
				prev_log_file.c_str());
			goto exit;
		}

		msg(thread_num, "The last %zu bytes were copied for %s.",
			total_bytes_copied, prev_log_file.c_str());
		destroy_log_tail();
		++m_last_log_num;
		goto repeat;
	}

	result = true;

exit:
	if (!result)
		destroy_log_tail();
	return result;
}

void BackupImpl::copy_log_file_job(size_t log_num, unsigned thread_num) {
	DBUG_ASSERT(log_num <= m_last_log_num);

	if (!m_tasks_group.get_result()) {
		msg(thread_num, "Skip copying %zu aria log file due to error", log_num);
		m_tasks_group.finish_task(0);
		return;
	}

// Copy log file if the file is not the last one.
	if (log_num < m_last_log_num) {
		std::string log_file = log_file_name(aria_log_dir_path().c_str(), log_num);
		if (!m_ds->copy_file(log_file.c_str(),
  			log_file_name_only(log_num).c_str(),
			thread_num, false)) {
			msg(thread_num, "Error on copying %s aria log file.", log_file.c_str());
			m_tasks_group.finish_task(0);
		}
		else
			m_tasks_group.finish_task(1);
		return;
	}
// Copy the last log file.
	m_tasks_group.finish_task(copy_log_tail(thread_num, false) ? 1 : 0);
}

void BackupImpl::destroy_log_tail() {
	if (m_last_log_src >= 0) {
		my_close(m_last_log_src, MYF(MY_WME));
		m_last_log_src = -1;
	}
	if (m_last_log_dst) {
		ds_close(m_last_log_dst);
		m_last_log_dst = nullptr;
	}
}

bool BackupImpl::wait_for_finish() {
	return m_tasks_group.wait_for_finish();
}

bool BackupImpl::copy_offline_tables(
	const std::unordered_set<table_key_t> *exclude_tables, bool no_lock,
	bool copy_stats) {
	DBUG_ASSERT(m_tasks_group.is_finished());

	std::vector<std::unique_ptr<Table>> ignored_tables;

	while (true) {
		std::unique_lock<std::mutex> lock(m_offline_tables_mutex);
		if (m_offline_tables.empty())
			break;
		auto table = std::move(m_offline_tables.back());
		m_offline_tables.pop_back();
		lock.unlock();
		if ((exclude_tables &&
			exclude_tables->count(table_key(table->get_db(), table->get_table()))) ||
			(!copy_stats && table->is_stats())) {
			ignored_tables.push_back(std::move(table));
			continue;
		}
		m_tasks_group.push_task(
			std::bind(&BackupImpl::process_table_job, this, table.release(), false,
					copy_stats, no_lock, std::placeholders::_1));
	}

	if (!ignored_tables.empty()) {
		std::lock_guard<std::mutex> lock(m_offline_tables_mutex);
		m_offline_tables = std::move(ignored_tables);
	}

	return true;
}

bool BackupImpl::finalize() {
	DBUG_ASSERT(m_tasks_group.is_finished());
	DBUG_ASSERT(!m_con_pool.empty());
	bool result = true;
	msg("Start copying statistics aria tables.");
	copy_offline_tables(nullptr, true, true);
	while (!m_tasks_group.is_finished())
		std::this_thread::sleep_for(std::chrono::milliseconds(1));
	msg("Stop copying statistics aria tables.");
	copy_log_tail(0, true);
	destroy_log_tail();
	return result;
}

Backup::Backup(const char *datadir_path,
		const char *aria_log_path,
               ds_ctxt_t *datasink,
	 std::vector<MYSQL *> &con_pool, ThreadPool &thread_pool) :
		m_backup_impl(
			new BackupImpl(datadir_path, aria_log_path,
			               datasink, opt_no_lock, con_pool,
				thread_pool)) { }

Backup::~Backup() {
	delete m_backup_impl;
}

bool Backup::init() {
	return m_backup_impl->init();
}

bool Backup::start(bool no_lock) {
	return m_backup_impl->start(no_lock);
}

bool Backup::wait_for_finish() {
	return m_backup_impl->wait_for_finish();
}

bool Backup::copy_offline_tables(
	const std::unordered_set<table_key_t> *exclude_tables, bool no_lock,
	bool copy_stats) {
	return m_backup_impl->copy_offline_tables(exclude_tables, no_lock,
		copy_stats);
}

bool Backup::finalize() {
	return m_backup_impl->finalize();
}

bool Backup::copy_log_tail() {
	return m_backup_impl->copy_log_tail();
}

void Backup::set_post_copy_table_hook(const post_copy_table_hook_t &hook) {
	m_backup_impl->set_post_copy_table_hook(hook);
}

bool prepare(const char *target_dir) {
	maria_data_root= (char *)target_dir;

	if (maria_init())
		die("Can't init Aria engine (%d)", errno);

	maria_block_size= 0;                          /* Use block size from file */
		/* we don't want to create a control file, it MUST exist */
	if (ma_control_file_open(FALSE, TRUE, TRUE, control_file_open_flags))
		die("Can't open Aria control file (%d)", errno);

	if (last_logno == FILENO_IMPOSSIBLE)
		die("Can't find any Aria log");

        LogFileCollection logs(target_dir, last_logno);
        logs.die_if_missing(last_logno); // Fatal, a broken backup.
        /*
          "mariadb-backup --backup" can put extra log files,
          with log number greater than last_logno. For example,
          this combination of files is possible:
          - aria_log_control  (with last_logno==1)
          - aria_log.00000001 (last_logno)
          - aria_log.00000002 (last_logno+1, the extra log file)
          This can happen if during the ealier run of
          "mariadb-backup --backup" a log rotate happened.
          The extra log file is copied to the backup directory,
          but last_logno in aria_log_control does not get updated.
          This mismatch is probably not good and should eventually be fixed.
          But during "mariadb-backup --prepare" this mismatch goes away:
          aria_log_control gets fixed to say last_logno==2.
          See mysql-test/suite/mariabackup/aria_log_rotate_during_backup.test,
          it covers the scenario with one extra file created during --backup.
        */
        logs.find_logs_after_last(target_dir);
        last_logno= logs.last(); // Update last_logno if extra logs were found

	if (init_pagecache(maria_pagecache, 1024L*1024L, 0, 0,
		static_cast<uint>(maria_block_size), 0, MY_WME) == 0)
		die("Got error in Aria init_pagecache() (errno: %d)", errno);

	if (init_pagecache(maria_log_pagecache, 1024L*1024L,
		0, 0, TRANSLOG_PAGE_SIZE, 0, MY_WME) == 0 ||
		translog_init(maria_data_root, TRANSLOG_FILE_SIZE,
		0, 0, maria_log_pagecache, TRANSLOG_DEFAULT_FLAGS, FALSE))
		die("Can't init Aria loghandler (%d)", errno);

	if (maria_recovery_from_log())
		die("Aria log apply FAILED");

	if (maria_recovery_changed_data || recovery_failures) {
		if (ma_control_file_write_and_force(last_checkpoint_lsn, last_logno,
			max_trid_in_control_file, 0))
			die("Aria control file update error");
// TODO: find out do we need checkpoint here
	}

	maria_end();
	return true;
}

} // namespace aria
