<?php
/**
 * Akeeba Engine
 * The PHP-only site backup engine
 *
 * @copyright Copyright (c)2006-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU GPL version 3 or, at your option, any later version
 * @package   akeebaengine
 */

namespace Akeeba\Engine\Core\Domain;

// Protection against direct access
defined('AKEEBAENGINE') or die();

use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Dump\Base as DumpBase;
use Akeeba\Engine\Factory;
use Psr\Log\LogLevel;

/**
 * Multiple database backup engine.
 */
class Db extends Part
{
	/** @var array A list of the databases to be packed */
	private $database_list = array();

	/** @var array The current database configuration data */
	private $database_config = null;

	/** @var DumpBase The current dumper engine used to backup tables */
	private $dump_engine = null;

	/** @var string The contents of the databases.ini file */
	private $databases_ini = '';

	/** @var array An array containing the database definitions of all dumped databases so far */
	private $dumpedDatabases = array();

	/** @var int Total number of databases left to be processed */
	private $total_databases = 0;

	/**
	 * Implements the constructor of the class
	 *
	 * @return  Db
	 */
	public function __construct()
	{
		parent::__construct();

		Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
	}

	/**
	 * Implements the _prepare abstract method
	 *
	 * @return  void
	 */
	protected function _prepare()
	{
		Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Preparing instance");

		// Populating the list of databases
		$this->populate_database_list();

		if ($this->getError())
		{
			return;
		}

		$this->total_databases = count($this->database_list);

		$this->setState('prepared');
	}

	/**
	 * Implements the _run() abstract method
	 *
	 * @return  void
	 */
	protected function _run()
	{
		if ($this->getState() == 'postrun')
		{
			Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Already finished");
			$this->setStep('');
			$this->setSubstep('');
		}
		else
		{
			$this->setState('running');
		}

		// Make sure we have a dumper instance loaded!
		if (is_null($this->dump_engine) && !empty($this->database_list))
		{
			Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Iterating next database");

			// Create a new instance
			$this->dump_engine = Factory::getDumpEngine(true);

			// Configure the dumper instance and pass on the volatile database root registry key
			$registry = Factory::getConfiguration();
			$rootkeys = array_keys($this->database_list);
			$root = array_shift($rootkeys);
			$registry->set('volatile.database.root', $root);

			$this->database_config = array_shift($this->database_list);
			$this->database_config['root'] = $root;
			$this->database_config['process_empty_prefix'] = ($root == '[SITEDB]') ? true : false;

			Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Now backing up $root ({$this->database_config['database']})");

			$this->dump_engine->setup($this->database_config);

			// Error propagation
			$this->propagateFromObject($this->dump_engine);

			if ($this->getError())
			{
				return;
			}
		}
		elseif (is_null($this->dump_engine) && empty($this->database_list))
		{
			$this->setError('Current dump engine died while resuming the step');

			return;
		}

		// Try to step the instance
		$retArray = $this->dump_engine->tick();

		// Error propagation
		$this->propagateFromObject($this->dump_engine);

		if ($this->getError())
		{
			return;
		}

		$this->setStep($retArray['Step']);
		$this->setSubstep($retArray['Substep']);

		// Check if the instance has finished
		if (!$retArray['HasRun'])
		{
			// Set the number of parts
			$this->database_config['parts'] = $this->dump_engine->partNumber + 1;

			// Push the definition
			array_push($this->dumpedDatabases, $this->database_config);

			// Go to the next entry in the list and dispose the old AkeebaDumperDefault instance
			$this->dump_engine = null;

			// Are we past the end of the list?
			if (empty($this->database_list))
			{
				Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: No more databases left to iterate");
				$this->setState('postrun');
			}
		}
	}

	/**
	 * Implements the _finalize() abstract method
	 *
	 * @return  void
	 */
	protected function _finalize()
	{
		$this->setState('finished');

		// If we are in db backup mode, don't create a databases.ini
		$configuration = Factory::getConfiguration();

		if (!Factory::getEngineParamsProvider()->getScriptingParameter('db.databasesini', 1))
		{
			Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Skipping databases.ini");
		}
		// Create the databases.ini contents
		elseif ($this->installerSettings->databasesini)
		{
			$this->createDatabasesINI();

			Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Creating databases.ini");

			// Create a new string
			$databasesINI = $this->databases_ini;

			// BEGIN FIX 1.2 Stable -- databases.ini isn't written on disk
			Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Writing databases.ini contents");

			$archiver = Factory::getArchiverEngine();
			$virtualLocation = (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'short') ? '' : $this->installerSettings->sqlroot;
			$archiver->addVirtualFile('databases.ini', $virtualLocation, $databasesINI);

			// Error propagation
			$this->propagateFromObject($archiver);

			if ($this->getError())
			{
				return;
			}
		}

		// On alldb mode, we have to finalize the archive as well
		if (Factory::getEngineParamsProvider()->getScriptingParameter('db.finalizearchive', 0))
		{
			Factory::getLog()->log(LogLevel::INFO, "Finalizing database dump archive");

			$archiver = Factory::getArchiverEngine();
			$archiver->finalize();

			// Error propagation
			$this->propagateFromObject($archiver);

			if ($this->getError())
			{
				return;
			}
		}

		// In CLI mode we'll also close the database connection
		if (defined('AKEEBACLI'))
		{
			Factory::getLog()->log(LogLevel::INFO, "Closing the database connection to the main database");
			Factory::unsetDatabase();
		}

		return;
	}

	/**
	 * Populates database_list with the list of databases in the settings
	 *
	 * @return void
	 */
	protected function populate_database_list()
	{
		// Get database inclusion filters
		$filters = Factory::getFilters();
		$this->database_list = $filters->getInclusions('db');

		// Error propagation
		$this->propagateFromObject($filters);

		if ($this->getError())
		{
			return;
		}

		if (Factory::getEngineParamsProvider()->getScriptingParameter('db.skipextradb', 0))
		{
			// On database only backups we prune extra databases
			Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Adding only main database");

			if (count($this->database_list) > 1)
			{
				$this->database_list = array_slice($this->database_list, 0, 1);
			}
		}
	}

	protected function createDatabasesINI()
	{
		// caching databases.ini contents
		Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Creating databases.ini data");

		// Create a new string
		$this->databases_ini = '';

		$blankOutPass = Factory::getConfiguration()->get('engine.dump.common.blankoutpass', 0);
		$siteRoot     = Factory::getConfiguration()->get('akeeba.platform.newroot', '');

		// Loop through databases list
		foreach ($this->dumpedDatabases as $definition)
		{
			$section = basename($definition['dumpFile']);

			$dboInstance = Factory::getDatabase($definition);
			$type = $dboInstance->name;
			$tech = $dboInstance->getDriverType();

			// If the database is a sqlite one, we have to process the database name which contains the path
			// At the moment we only handle the case where the db file is UNDER site root
			if ($tech == 'sqlite')
			{
				$definition['database'] = str_replace($siteRoot, '#SITEROOT#', $definition['database']);
			}

			if ($blankOutPass)
			{
				$this->databases_ini .= <<<ENDDEF
[$section]
dbtype = "$type"
dbtech = "$tech"
dbname = "{$definition['database']}"
sqlfile = "{$definition['dumpFile']}"
dbhost = "{$definition['host']}"
dbuser = ""
dbpass = ""
prefix = "{$definition['prefix']}"
parts = "{$definition['parts']}"

ENDDEF;

			}
			else
			{
				// We have to escape the password
				$escapedPassword = addcslashes($definition['password'], "\"\\\n\r");

				$this->databases_ini .= <<<ENDDEF
[$section]
dbtype = "$type"
dbtech = "$tech"
dbname = "{$definition['database']}"
sqlfile = "{$definition['dumpFile']}"
dbhost = "{$definition['host']}"
dbuser = "{$definition['username']}"
dbpass = "$escapedPassword"
prefix = "{$definition['prefix']}"
parts = "{$definition['parts']}"

ENDDEF;
			}
		}
	}

	/**
	 * Implements the getProgress() percentage calculation based on how many
	 * databases we have fully dumped and how much of the current database we
	 * have dumped.
	 *
	 * @return  float
	 */
	public function getProgress()
	{
		if ($this->total_databases)
		{
			return 0;
		}

		// Get the overall percentage (based on databases fully dumped so far)
		$remaining_steps = count($this->database_list);
		$remaining_steps++;
		$overall = 1 - ($remaining_steps / $this->total_databases);

		// How much is this step worth?
		$this_max = 1 / $this->total_databases;

		// Get the percentage done of the current database
		$local = $this->dump_engine->getProgress();

		$percentage = $overall + $local * $this_max;

		if ($percentage < 0)
		{
			$percentage = 0;
		}
		elseif ($percentage > 1)
		{
			$percentage = 1;
		}

		return $percentage;
	}
}
