<?php

/**
 * Author: Mikołaj `iClyde` Chodorowski
 * Contact: kontakt@iclyde.pl
 * Package: Backup Migration – WP Plugin
 * Version: 2.0
 */

// Namespace
namespace BMI\Plugin\Database;

// Use
use BMI\Plugin\BMI_Logger AS Logger;
use BMI\Plugin\Progress\BMI_ZipProgress AS Progress;


// // Exit on direct access
if (!defined('ABSPATH')) exit;

/**
 * Database sort method
 * Usage: Create new object and use "sortUnsorted" function
 */
class BMI_Database_Sorting {

  /**
   * Path to database files made with V2 engine
   */
  private $db_files_root;
  private $output_file_stream;
  private $name_of_finished_tables_dir = 'bmi_converted_completed_tables';
  private $progress;
  private $isCLI;
  private $unwantedTables = [
    'wfblockediplog',
    'wfblocks7',
    'wfcrawlers',
    'wffilechanges',
    'wffilemods',
    'wfhits',
    'wfhoover',
    'wfissues',
    'wfknownfilelist',
    'wflivetraffichuman',
    'wflocs',
    'wflogins',
    'wfnotifications',
    'wfpendingissues',
    'wfreversecache',
    'wfsnipcache',
    'wfstatus',
    'wftrafficrate',
    'actionscheduler_logs',
    'slim_stats',
    'woocommerce_sessions',
    'yoast_indexable',
    'slim_events',
    'cerber_files',
    'cerber_traffic',
    'cerber_log',
    'cerber_countries',
    'cerber_blocks',
    'cerber_acl'
  ];

  /**
   * __construct - description
   *
   * @param  {string} $db_root Path to database files
   * @return void
   */
  function __construct($db_root, &$progress, $isCLI) {

    /**
     * Set path to the root directory
     */
    $this->db_files_root = $db_root;
    $this->progress = $progress;
    $this->isCLI = $isCLI;

  }

  /**
   * sortUnsorted - Main function which dynamically sorts the tables (main function)
   *
   * @param  {array} $processData = []    Data of process to be continued or empty array to start
   * Requires keys [ 'index', 'insert', 'file', 'table_name', 'dir', 'unique', 'destinationFile' ]
   * Optional key: convertionFinished
   *
   * @return array of [ 'index', 'insert', 'file', 'table_name', 'dir', 'unique', 'destinationFile' ]
   */
  public function sortUnsorted($processData = []) {

    // Sort the SQL files into directories
    $this->putDatabaseFilesInDirectories();

    // Split files into partial files (query per batch)
    $process = $this->splitFilesInSeparateDirectories($processData);

    // Check if all tables finished and move them outside completed directory
    if ($this->doesTablesFinished()) {

      // Move them outside
      $this->moveAllConvertedTables();

      // Check if process finished
      if ($this->isCLI || (is_array($process) && array_key_exists('completed', $process) && $process['completed'] == 'yes')) {
        $process['convertionFinished'] = 'yes';
      }

    }

    // Return process for that
    return $process;

  }

  /**
   * doesTablesFinished - Checks if there is any table that didn't finish
   *
   * @return {bool} true or false
   */
  private function doesTablesFinished() {

    // Scan all files in root
    $allFiles = scandir($this->db_files_root);
    $files = array_diff($allFiles, array('.', '..', $this->name_of_finished_tables_dir));

    // If no other directories here that means nothing to convert left
    if (sizeof($files) <= 0) {

      // Return true for that
      return true;

    } else {

      // Otherwise it's false
      return false;

    }

  }

  /**
   * putDatabaseFilesInDirectories - It puts all SQL files into table directories
   * Does nothing if the root directory does not contain any SQL files
   *
   * @return void
   */
  private function putDatabaseFilesInDirectories() {

    // Scan all files in root
    $allFiles = scandir($this->db_files_root);
    $files = array_diff($allFiles, array('.', '..'));

    // For each found file make sure it's SQL and run merge function
    foreach ($files as $index => $filename) {

      // Exclude non .sql files
      if (substr($filename, -4) != '.sql') continue;

      // It may be directory with .sql ending, make sure it's not
      $pathToFile = $this->db_files_root . DIRECTORY_SEPARATOR . $filename;
      if (is_dir($pathToFile)) continue;

      // Merge the file
      $this->mergeFileWithDirectory($filename);

    }

  }

  /**
   * mergeFileWithDirectory - Merges the file into proper directory
   *
   * @param  {string} $filename File name which is located in root
   * @return bool true on success false on failed file move
   */
  private function mergeFileWithDirectory($filename) {

    // Suffix size which stands for ending ".sql"
    $suffix_size = 4;

    // Match output for table name with different name than just table
    // It may happen when our support used their tools for optimization
    $match_output = [];
    preg_match('/_(\d+)\_of\_(\d+)/', $filename, $match_output);

    // If that's the case, add suffix size as we don't need that info here
    if (sizeof($match_output) > 0) {
      $suffix_size += strlen($match_output[0]);
    }

    // If we know the suffix size, get table name
    $table_name = substr($filename, 0, -$suffix_size);

    // Prepare future paths of the file
    $dest_dir = $this->db_files_root . DIRECTORY_SEPARATOR . $table_name;
    $source_path = $this->db_files_root . DIRECTORY_SEPARATOR . $filename;
    $dest_path = $dest_dir . DIRECTORY_SEPARATOR . $filename;

    // Check if the directory exists and if it's not make new one
    if (!file_exists($dest_dir) && !is_dir($dest_dir)) {
      @mkdir($dest_dir, 0755, true);
    }

    // Check if the files is not an duplicate and make sure both will be merged
    $tries = 2;
    while (file_exists($dest_path)) {

      // If file exists already add number at the suffix and try again until unique name
      $dest_path = substr($dest_path, 0, -4) . '_' . $tries . '.sql';
      $tries++;

    }

    // If we're sure move the file to correct directory
    if (rename($source_path, $dest_path)) {

      // Return true on success
      return true;

    } else {

      // Return false on fail
      return false;

    }

  }

  /**
   * countAllFilesAndQueries - Counts all files inside directory and returns all query count
   *
   * @return {array} of total_queries, total_size, all_tables and all_files
   */
  public function countAllFilesAndQueries() {

    // Scan all files in root
    $allFiles = scandir($this->db_files_root);
    $folders = array_diff($allFiles, array('.', '..', $this->name_of_finished_tables_dir));

    // Tables
    $tables = [];

    // Files
    $files = [];

    // Total size
    $total_size = 0;

    // For each found file make sure it's SQL and run merge function
    foreach ($folders as $index => $filename) {

      // Path to that file/directory
      $pathToFile = $this->db_files_root . DIRECTORY_SEPARATOR . $filename;

      // Hanlde only dirs
      if (is_dir($pathToFile)) {

        // Add table information
        $tables[$filename] = 0;

        // Scan SQL files
        $allSQLFiles = scandir($pathToFile);
        $allSQLFilesParsed = array_diff($allSQLFiles, array('.', '..'));

        // Go throught these files
        foreach ($allSQLFilesParsed as $indexSQL => $filenameSQL) {

          // Exclude non .sql files
          if (substr($filenameSQL, -4) != '.sql') continue;

          // Get size of that file
          $size = filesize($pathToFile . DIRECTORY_SEPARATOR . $filenameSQL);

          // Add size in total and table
          $tables[$filename] += $size;
          $total_size += $size;

          // Add that file to all files
          $files[] = $filenameSQL;

        }

      }

    }

    // Prepare stats variable
    $stats = [
      'total_queries' => (sizeof($files) * 5),
      'total_size' => $total_size,
      'all_tables' => $tables,
      'all_files' => $files
    ];

    return $stats;

  }

  /**
   * countTablesDuringProcess - Counts tables even during process
   *
   * @return array $done, $progress, $total
   */
  private function countTablesDuringProcess() {

    $theFinalCount = 0;
    $progressCount = 0;
    $doneCount = 0;

    // Scan all files in root (now everything should be directory)
    $allFiles = scandir($this->db_files_root);
    $files = array_diff($allFiles, array('.', '..', $this->name_of_finished_tables_dir));

    foreach ($files as $index => $filename) {
      if (is_dir($this->db_files_root . DIRECTORY_SEPARATOR . $filename)) {
        $theFinalCount++;
        $progressCount++;
      }
    }

    $doneDir = $this->db_files_root . DIRECTORY_SEPARATOR . $this->name_of_finished_tables_dir;
    if (file_exists($doneDir) && is_dir($doneDir)) {

      $allDoneFiles = scandir($doneDir);
      $doneFiles = array_diff($allDoneFiles, array('.', '..'));

      foreach ($doneFiles as $index => $filename) {
        if (is_dir($doneDir . DIRECTORY_SEPARATOR . $filename)) {
          $theFinalCount++;
          $doneCount++;
        }
      }

    }

    return [
      'done' => $doneCount,
      'in_progress' => $progressCount,
      'total' => $theFinalCount
    ];

  }

  /**
   * logFinished - Shows progress of the process in the log file
   *
   * @return void
   */
  private function logFinished($dir, $filename) {

    $counts = $this->countTablesDuringProcess();

    $progress = ($counts['done'] + 1) . '/' . $counts['total'] . ' (' . number_format((($counts['done'] + 1) / $counts['total']) * 100, 2) . '%)';
    $translated = __('Finished %progress%: %filename% table, total of %parts% parts.', 'backup-backup');

    // Get part amount
    $partsAll = scandir($dir);
    $parts = sizeof(array_diff($partsAll, array('.', '..', $this->name_of_finished_tables_dir)));

    $theLog = str_replace('%progress%', $progress, $translated);
    $theLog = str_replace('%filename%', $filename, $theLog);
    $theLog = str_replace('%parts%', $parts, $theLog);

    $this->progress->log($theLog, 'SUCCESS');

    $percentage = number_format((($counts['done'] + 1) / $counts['total']) * 100, 2);
    $this->progress->progress((50 + ($percentage / 4)), 'INFO');

  }

  /**
   * splitInDirectories - Splits file into parts inside proper directory
   * @param {array} $processData Process data to be continued
   *
   * @return void
   */
  private function splitFilesInSeparateDirectories($processData = []) {

    // Local process variable
    $process = [];

    // Scan all files in root (now everything should be directory)
    $allFiles = scandir($this->db_files_root);
    $files = array_diff($allFiles, array('.', '..', $this->name_of_finished_tables_dir));

    // Loop all tables and split them one by one
    foreach ($files as $index => $filename) {

      // Prepate path to the SQL file which should be converted
      $dir = $this->db_files_root . DIRECTORY_SEPARATOR . $filename;

      // Always double check if it's directory otherwise we may get some unexpected errors
      if (!is_dir($dir)) continue;

      // Log the new file which will be processed
      $destinationFile = $this->makeDestinationFilename($dir, $filename);
      $destinationIndex = $destinationFile['index'];

      // Log only if it's first batch
      if ($destinationIndex == 1) {
        $this->progress->log(str_replace('%s', $filename, __('Starting conversion of: %s table.', 'backup-backup')), 'INFO');
      }

      // Dynamically find file to shrink and split it
      $process = $this->splitFileInTableDirectory($dir, $filename, $processData);

      // If it's not CLI use batching
      if (!$this->isCLI) {

        // If process finished
        if (is_array($process) && array_key_exists('completed', $process) && $process['completed'] == 'yes') {

          // Show in the log file the progress
          $this->logFinished($dir, $filename);

          // Check if directory for completed tables exist and make it if it is not
          $doneDirectory = $this->db_files_root . DIRECTORY_SEPARATOR . $this->name_of_finished_tables_dir;
          if (!file_exists($doneDirectory) && !is_dir($doneDirectory)) {
            @mkdir($doneDirectory, 0755, true);
          }

          // Move the table into completed directory
          rename($dir, $doneDirectory . DIRECTORY_SEPARATOR . $filename);

          // Return process even empty
          return $process;

        } else {

          // If didn't finished that table return process data for next batch to finish
          return $process;

        }

      } else {

        // Show in the log file the progress
        $this->logFinished($dir, $filename);

        // Check if directory for completed tables exist and make it if it is not
        $doneDirectory = $this->db_files_root . DIRECTORY_SEPARATOR . $this->name_of_finished_tables_dir;
        if (!file_exists($doneDirectory) && !is_dir($doneDirectory)) {
          @mkdir($doneDirectory, 0755, true);
        }

        // Move the table into completed directory
        rename($dir, $doneDirectory . DIRECTORY_SEPARATOR . $filename);

      }

    }

  }

  /**
   * splitFileInTableDirectory - Splits file into parts for easier progress read and performance
   *
   * @param  {string} $dir        Path to parent directory of table SQL files
   * @param  {string} $table_name Table name (current directory name)
   * @param  {array}  $processData Process data to be continued
   * @return void
   */
  private function splitFileInTableDirectory($dir, $table_name, $processData = []) {

    // Prepare local var for process data
    $process = [];

    // Get all database files inside that directory (it can contain multiple files alrady)
    $allFiles = scandir($dir);
    $files = array_diff($allFiles, array('.', '..'));

    // Loop throught all of them and apply splitting
    foreach ($files as $index => $filename) {

      // Just to triple check that parent is a valid directory
      if (!is_dir($dir)) continue;

      // Prepare exact path to the SQL file
      $file = $dir . DIRECTORY_SEPARATOR . $filename;

      // Run splitting function for that file
      $process = $this->splitDatabaseFile($file, $dir, $table_name, $processData);

      // Break each query if not CLI
      if (!$this->isCLI) {
        break;
      }

    }

    // If CLI only rename files without return
    if ($this->isCLI) {

      // Rename splited files
      $this->renameSplitedFiles($dir);

    } else {

      // Check if process for that table completed and rename if so
      if (is_array($process) && array_key_exists('completed', $process) && $process['completed'] == 'yes') {

        // Rename splited files
        $this->renameSplitedFiles($dir);

        // Return empty process data as we will iterate new file in next batch
        return [
          'index' => 0,
          'insert' => 'no',
          'completed' => 'yes'
        ];

      } else {

        // Or just return process data for future use (next batch)
        return $process;

      }

    }

  }

  /**
   * logPartial - Logs partial progress to the output log
   *
   * @param  {string} $dir        Directory of the table
   * @param  {string} $table_name Name of the table
   * @return void
   */
  private function logPartial($dir, $table_name) {

    // Get part amount
    $partsAll = scandir($dir);
    $parts = sizeof(array_diff($partsAll, array('.', '..', $this->name_of_finished_tables_dir)));
    $parts = $parts - 1;

    // Prepare log string
    $logString = __('Finished part %part% of %table% table.', 'backup-backup');
    $logString = str_replace('%table%', $table_name, $logString);
    $logString = str_replace('%part%', $parts, $logString);

    // Log the string to output
    $this->progress->log($logString, 'INFO');

  }

  /**
   * splitDatabaseFile - Splits a SQL file into parts
   *
   * @param  {string} $file       Name of the SQL File
   * @param  {string} $dir        Full path to the SQL file
   * @param  {string} $table_name Name of the table to which SQL file belong
   * @param  {array}  $processData Process data to be continued
   * @return void
   */
  private function splitDatabaseFile($file, $dir, $table_name, $processData = []) {

    // Make destionation file and prepare output file stream
    $destinationFile = $this->makeDestinationFilename($dir, $table_name);
    $destinationFile = $destinationFile['file'];

    // Local variables required for the process
    $table_unique = 0;
    $is_converted = false;
    $query_started = false;
    $custom_vars_started = false;
    $values_started = false;
    $insert_next = false;
    $last_seek = 0;

    // Resolve not finished process
    if (!$this->isCLI && is_array($processData) && !empty($processData)) {

      // Should be file path $file_path & $file (check if the file exist)
      if (array_key_exists('file', $processData) && file_exists($processData['file'])) {

        // Set existing file path
        $file = $processData['file'];

        // Should be int<make sure> of $last_seek
        if (array_key_exists('index', $processData)) {
          $last_seek = intval($processData['index']) + 1;
        }

        // Yes | No values {string} of $insert_next
        if (array_key_exists('insert', $processData)) {
          $ins = $processData['insert'];

          if ($ins == 'yes') $insert_next = true;
          else $insert_next = false;
        }

        // Table name of $table_name
        if (array_key_exists('table_name', $processData)) {
          $table_name = $processData['table_name'];
        }

        // Directory of that table $dir
        if (array_key_exists('dir', $processData)) {
          $dir = $processData['dir'];
        }

        // Unique for that table $table_unique
        if (array_key_exists('unique', $processData)) {
          $table_unique = $processData['unique'];
        }

        // Destination File (output) of $destinationFile
        if (array_key_exists('destinationFile', $processData)) {
          $destinationFile = $processData['destinationFile'];
        }

      }

    }

    // Open stream for output file
    $this->output_file_stream = fopen($destinationFile, 'a+');

    // Keep original filename
    $file_path = $file;

    // Open SplObject for seek-based reading
    $file = new \SplFileObject($file);

    // Go to last line to check how many lines are there
    $file->seek($file->getSize());

    // Calculate total lines in that file
    $total_lines = $file->key() + 1;

    for ($i = $last_seek; $i < $total_lines; ++$i) {

      // Start seek frrom the loop index
      $file->seek($i);

      // Get line into string and trim it (it contains new line at the end)
      $line = trim($file->current());

      // Variable for that line, if it's a comment or not
      $hasComment = false;

      // Check this only if the size of line is below 24
      // Otherwise it's not a comment line and we can ignore that check for better performance
      if (strlen($line) < 24) {

        // List of all our comments
        $c_fs = "/* CONVERTED DB FILE */";
        $q_s = "/* QUERY START */";
        $q_e = "/* QUERY END */";
        $cq_s = "/* CUSTOM VARS START */";
        $cq_e = "/* CUSTOM VARS END */";
        $v_s = "/* VALUES START */";
        $v_e = "/* VALUES END */";

        // If it's converted already ignore the file
        if ($line === $c_fs) {

          // Mark as converted (it output file be removed by this)
          $is_converted = true;

          // And break the loop, no need to continue
          break;

        }

        // Check if it's a query start comment and adjust local variables for that for future know
        if ($line === $q_s) {
          $query_started = true;
          $hasComment = true;
        }

        // Check if it's a custom variable section start comment and adjust local variables for that for future know
        if ($line === $cq_s) {
          $custom_vars_started = true;
          $hasComment = true;
        }

        // Check if it's a values section start comment and adjust local variables for that for future know
        if ($line === $v_s) {
          $values_started = true;
          $hasComment = true;
        }

        // If we are behind a heading insert this line into new output file
        if ($insert_next && $hasComment) {

          // Add new line as we trimed it before
          $line .= "\n";

          // Push this to output file
          $this->insertIntoOutputFile($line, $table_name);

          // If we already added this line continue to new line
          continue;

        }

        // Handle query ending comment
        if ($line === $q_e) {

          // Adjust local variable for that file
          $query_started = false;

          // If it's below heading, add new output and generate new heading for output file
          if ($insert_next) {

            // Add double ending – it looks much better for our eyes :)
            $line .= "\n\n";

            // Add this line
            $this->insertIntoOutputFile($line, $table_name);

            // Close the file as we split per query, we will need new one for new query
            if (is_resource($this->output_file_stream) || get_resource_type($this->output_file_stream) === 'file') {
              fclose($this->output_file_stream);
            }

            // Check if it's end of the file add 4 lines offset for empty lines
            // If it's not the end make new heading, otherwise remove new output file
            if (($i + 4) < $total_lines) {

              // Make new destination file
              $destinationFile = $this->makeDestinationFilename($dir, $table_name);
              $destinationFile = $destinationFile['file'];

              // Check if we really need that table contents
              if ($this->checkIfContainName($table_name, $this->unwantedTables)) {

                $this->progress->log(str_replace('%s', $table_name, __('Cleaning up contents of %s table.', 'backup-backup')), 'INFO');

                $insert_next = false;

                if (!$this->isCLI) {

                  $file = null;

                  if (is_resource($this->output_file_stream) || get_resource_type($this->output_file_stream) === 'file') {
                    fclose($this->output_file_stream);
                  }

                  if (file_exists($destinationFile)) {
                    @unlink($destinationFile);
                  }

                    if (file_exists($file_path)) {
                    @unlink($file_path);
                  }

                  // Return empty for new file
                  return [
                    'index' => 0,
                    'insert' => 'no',
                    'completed' => 'yes'
                  ];

                }

              } else {

                // Log partial progress
                $this->logPartial($dir, $table_name);

                // Open new stream for new output file
                $this->output_file_stream = fopen($destinationFile, 'a+');

                // Generate new heading
                $heading = $this->generateNewHeading($table_unique, $table_name);

                // Put the heading into new file
                $this->insertIntoOutputFile($heading, $table_name);

                // Technically only few bytes but with 200 tables it may be few MBs :)
                // Always unset variables if you don't need them anymore
                unset($heading);

              }

              if (!$this->isCLI) {

                // Check if the output file is open and close it if it is
                $file = null;
                if (is_resource($this->output_file_stream) || get_resource_type($this->output_file_stream) === 'file') {
                  fclose($this->output_file_stream);
                }

                // Return latest insert query
                return [
                  'index' => $i,
                  'insert' => ($insert_next == true ? 'yes' : 'no'),
                  'file' => $file_path,
                  'table_name' => $table_name,
                  'dir' => $dir,
                  'unique' => $table_unique,
                  'destinationFile' => $destinationFile
                ];

              }

            } else {

              // Log partial progress
              $this->logPartial($dir, $table_name);

            }

            // Continue to new line as we handled this one already
            continue;

          }

        }

        // Handle Custom Variables ending comment
        if ($line === $cq_e) {

          // Adjust local variables for next loops
          $custom_vars_started = false;

        }

        // Handle values ending
        if ($line === $v_e) {

          // Adjust local variables for next loops
          $values_started = false;

          // If it's after heading add this line into new output file
          if ($insert_next) {

            // Add new line as we trimed it before
            $line .= "\n";

            // Insert that line into new file
            $this->insertIntoOutputFile($line, $table_name);

            // As we added the line continue to new one
            continue;

          }

        }

      }

      // Check if the line is not a comment
      if (!$hasComment) {

        // If the line should be a part of query handle that case
        if ($query_started === true) {

          // If it's after heading allow to add this line to new output file
          if ($insert_next) {

            // Add new line as we trimed it earlier
            $line .= "\n";

            // Insert the line into new file
            $this->insertIntoOutputFile($line, $table_name);

            // As we added that line continue to new one
            continue;

          }

        }

        // Handle custom variables
        if ($custom_vars_started === true) {

          // We are interested only into PRE TABLE NAME which contains unique timestamp
          if (strpos($line, 'PRE_TABLE_NAME')) {

            // Prepare variable for preg match output
            $output = [];

            // Run the preg match function to find the
            preg_match('/\`(.*)\`/', $line, $output);

            // Get the unique value from regex output
            $unique = substr($output[1], 0, 10);

            // Set the local variable to found unique for future use in the heading
            $table_unique = $unique;

            // As we went throught old heading let the script know that it can insert new lines in next loops
            $insert_next = true;

            // Prepare new heading for file which will be used in next loops
            $heading = $this->generateNewHeading($unique, $table_name);

            // Insert that heading
            $this->insertIntoOutputFile($heading, $table_name);

          }

        }

        // Handle casual variable line
        if ($values_started === true) {

          // If it can insert new output handle it
          if ($insert_next) {

            // Insert that line into new file
            $this->insertIntoOutputFile($line, $table_name);

            // Free up the memory as this line may be actually above few MBs.
            unset($line);

            // Continue to new line as we inserted this one already
            continue;

          }

        }

      }

      // Free up the memory as this line may be actually above few MBs.
      unset($line);

    }

    // $total_lines
    // File size: $file->getSize() / 1024 / 1024

    // Check if the output file is open and close it if it is
    if (is_resource($this->output_file_stream) || get_resource_type($this->output_file_stream) === 'file') {
      fclose($this->output_file_stream);
    }

    // If the file was already converted
    if ($is_converted === false) {

      // Empty the SplFileObject stream
      $file = null;

      // Remove new output file as there's nothing to do with it
      @unlink($file_path);

    } else {

      // If the convert finished, remove source file as we now have splited files
      @unlink($destinationFile);

    }

    // Empty SplFileObject stream
    $file = null;

    // Only if not CLI
    if (!$this->isCLI) {

      // Return empty process data as we will iterate new file in next batch
      return [
        'index' => 0,
        'insert' => 'no',
        'completed' => 'yes'
      ];

    }

  }

  /**
   * makeHeading - Generates new heading (converted) for output file
   *
   * @param  {string/int} $unique       Unique timestamp generated during backup (all tables have to be part of it)
   * @param  {string}     $table_name   Table name of current file, required to generate the heading
   * @return {string}                   New heading
   */
  private function generateNewHeading($unique, $table_name) {

    $heading = '';

    $heading .= "/* CONVERTED DB FILE */\n\n";
    $heading .= "/* QUERY START */\n";
    $heading .= "SET foreign_key_checks = 0;\n";
    $heading .= "/* QUERY END */\n";
    $heading .= "\n";
    $heading .= "/* QUERY START */\n";
    $heading .= "SET SQL_MODE = '';\n";
    $heading .= "/* QUERY END */\n";
    $heading .= "\n";
    $heading .= "/* QUERY START */\n";
    $heading .= "SET time_zone = '+00:00';\n";
    $heading .= "/* QUERY END */\n";
    $heading .= "\n";
    $heading .= "/* QUERY START */\n";
    $heading .= "SET NAMES 'utf8';\n";
    $heading .= "/* QUERY END */\n";
    $heading .= "\n";
    $heading .= "/* CUSTOM VARS START */\n";
    $heading .= "/* REAL_TABLE_NAME: `" . $table_name . "`; */\n";
    $heading .= "/* PRE_TABLE_NAME: `" . $unique . "_" . $table_name . "`; */\n";
    $heading .= "/* CUSTOM VARS END */\n";
    $heading .= "\n";

    return $heading;

  }

  /**
   * insertIntoOutputFile - Puts and output data into new (current) file
   *
   * @param  {string} &$line      Pointer to line, to save the memory
   * @param  {string} $table_name Table name, may be useful for future use
   * @return void
   */
  private function insertIntoOutputFile(&$line, $table_name) {

    if (is_resource($this->output_file_stream) || get_resource_type($this->output_file_stream) === 'file') {
      fwrite($this->output_file_stream, $line);
    }

  }

  /**
   * destinationFile - Makes unique destionation file name
   *
   * @param  {string} $dir        Full path to the SQL file
   * @param  {string} $table_name Name of the table to which SQL file belong
   * @return {string} name of the destionation SQL file
   */
  private function makeDestinationFilename($dir, $table_name) {

    // Start from file number one
    $i = 1;

    // Make an assumption that it will be the first file
    $file = $table_name . '_' . $i . '.sql';

    // Loop until it find unqiue name
    while (file_exists($dir . DIRECTORY_SEPARATOR . $file)) {

      // Increment each iteration for new filename
      $i++;

      // Set new name to check
      $file = $table_name . '_' . $i . '.sql';

    }

    // Return final file name
    return ['file' => $dir . DIRECTORY_SEPARATOR . $file, 'index' => $i];

  }

  /**
   *
   * renameSplitedFiles - Renames all files after splitting into sorted SQL files
   *                      It also gives better progress experience for the user
   *
   * @param  {string} $directory Path to directory where the rename function should run
   * @return void
   */
  private function renameSplitedFiles($directory) {

    // Scan all files after splitting
    $allFiles = scandir($directory);
    $files = array_diff($allFiles, array('.', '..'));

    // Loop throught them and add proper number for each
    foreach ($files as $index => $filename) {

      // Make new destination name
      $newName = substr($filename, 0, -4) . '_of_' . sizeof($files) . '.sql';

      // Current file path
      $file = $directory . DIRECTORY_SEPARATOR . $filename;

      // Final path (after rename)
      $finalPathWithName = $directory . DIRECTORY_SEPARATOR . $newName;

      // Finally rename the file
      rename($file, $finalPathWithName);

    }

  }

  /**
   * checkIfContainName - Checks if A (string) is part of B array
   *
   * @return bool
   */
  private function checkIfContainName($tableName, $unwantedTables) {

    $found = false;

    $tableName = str_replace('-', '', $tableName);
    $tableName = str_replace('_', '', $tableName);
    $tableName = strtolower($tableName);

    for ($i = 0; $i < sizeof($unwantedTables); ++$i) {

      $name = $unwantedTables[$i];
      $name = str_replace('-', '', $name);
      $name = str_replace('_', '', $name);
      $name = strtolower($name);

      if (strpos($tableName, $name) !== false) {
        $found = true;
        break;
      }

    }

    return $found;

  }

  /**
   * moveAllConvertedTables - Moves all completed tables outside
   *
   * @return void
   */
  private function moveAllConvertedTables() {

    // Finished directory path
    $finishedDBDir = $this->db_files_root . DIRECTORY_SEPARATOR . $this->name_of_finished_tables_dir;

    // Check if it exists
    if (!file_exists($finishedDBDir) || !is_dir($finishedDBDir)) {
      return;
    }

    // Scan all tables in finished directory
    $allFiles = scandir($finishedDBDir);
    $files = array_diff($allFiles, array('.', '..'));

    // Loop throught them and move outside
    foreach ($files as $index => $filename) {

      // Old path
      $old = $this->db_files_root . DIRECTORY_SEPARATOR . $this->name_of_finished_tables_dir . DIRECTORY_SEPARATOR . $filename;

      // New path
      $new = $this->db_files_root . DIRECTORY_SEPARATOR . $filename;

      // Move that directory to parent
      rename($old, $new);

    }

    // Unlink directory that won't be used as it's (and it should) be empty.
    if (file_exists($finishedDBDir) && is_dir($finishedDBDir)) {
      @rmdir($finishedDBDir);
    }

    // Return void
    return;

  }

}
