<?php
/**
 * @package Legacy
 * @version $Id$
 */

if (!defined('XOOPS_ROOT_PATH')) exit();

require_once XOOPS_LEGACY_PATH . "/admin/class/ModuleUtils.class.php";
require_once XOOPS_LEGACY_PATH . "/admin/class/ModuleInstallUtils.class.php";

/**
 * @brief The framework for the phased update.
 * 
 * @section Description
 * 
 * You can make your own custom-update-installer for your modules with the
 * sub-class of this class. It's easy to make by many utility functions. You
 * can write your sub-class as well as batch files.
 * 
 * On Legacy System module, upgrade is called when users try to update. So you
 * must implement your sub-class for also correct update. For example, the
 * custom-update-install have to update module templates & block templates,
 * because users expect that the module-update function does it.
 * 
 * For the custom-update-install, Legacy_ModuleInstallUtils is good for you.
 * Plus, this class has some usefull static methods for upgrade. Such functions
 * have notes as "The utility method for the custom-update-installer".
 * 
 * And, this class as the template-pattern has some methods you may override.
 * These methods have note as "You may do custom".
 * 
 * @section Convention
 * 
 * Module Update function build the current-$xoopsModule from DB, and then sets
 * it to this class through setCurrentXoopsModule(). Basically, you can access
 * it by $this->_mCurrentXoopsModule. And, that function build the
 * target-$xoopsModule from xoops_version, and then set it to this class through
 * setTargetXoopsModule(). Also you can access it by $this->_mTargetXoopsModule.
 * 
 * @see Legacy_ModuleInstallUtils
 */
class Legacy_ModulePhasedUpgrader
{
	/**
	 * This is an array of milestone version informations. Key is a version
	 * number. Value is a method name called by execute().
	 * 
	 * Format:
	 * {version} => {methodName}
	 * 
	 * Example:
	 * var $_mMilestone = array('020' => 'update020', '025' => 'update025');
	 * 
	 * @access protected
	 */
	var $_mMilestone = array();
	
	/**
	 * This instance is prepared automatically in the constructor.
	 * 
	 * @public
	 * @var Legacy_ModuleUtilsSimpleLog
	 */
	var $mLog = null;
	
	/**
	 * @var XoopsModule
	 */
	var $_mCurrentXoopsModule;
	
	/**
	 * @var int
	 */
	var $_mCurrentVersion;

	/**
	 * @var XoopsModule
	 */
	var $_mTargetXoopsModule;
	
	/**
	 * @var int
	 */
	var $_mTargetVersion;
	
	/**
	 * @var bool
	 */
	var $_mForceMode = false;
	
	function Legacy_ModulePhasedUpgrader()
	{
		$this->mLog =& new Legacy_ModuleUtilsSimpleLog();
	}
	
	/**
	 * Sets a value indicating whether the force mode is on.
	 * @param bool $isForceMode
	 */
	function setForceMode($isForceMode)
	{
		$this->_mForceMode = $isForceMode;
	}
	
	/**
	 * Sets the current XoopsModule. This method creates the clone of this
	 * object to prevent cache of the module handler, and then keep it to the
	 * property. Plus, this method copies the version value of this object to
	 * the _mCurrentVersion as backup for the case where the value of this
	 * object is changed for updating.
	 * 
	 * @public
	 * @param XoopsModule $xoopsModule
	 */
	function setCurrentXoopsModule(&$xoopsModule)
	{
		$handler =& xoops_gethandler('module');
		$cloneModule =& $handler->create();
		
		$cloneModule->unsetNew();
		$cloneModule->set('mid', $xoopsModule->get('mid'));
		$cloneModule->set('name', $xoopsModule->get('name'));
		$cloneModule->set('version', $xoopsModule->get('version'));
		$cloneModule->set('last_update', $xoopsModule->get('last_update'));
		$cloneModule->set('weight', $xoopsModule->get('weight'));
		$cloneModule->set('isactive', $xoopsModule->get('isactive'));
		$cloneModule->set('dirname', $xoopsModule->get('dirname'));
		$cloneModule->set('hasmain', $xoopsModule->get('hasmain'));
		$cloneModule->set('hasadmin', $xoopsModule->get('hasadmin'));
		$cloneModule->set('hassearch', $xoopsModule->get('hassearch'));
		$cloneModule->set('hasconfig', $xoopsModule->get('hasconfig'));
		$cloneModule->set('hascomments', $xoopsModule->get('hascomments'));
		$cloneModule->set('hasnotification', $xoopsModule->get('hasnotification'));
		
		$this->_mCurrentXoopsModule =& $cloneModule;
		$this->_mCurrentVersion = $cloneModule->get('version');
	}
	
	/**
	 * Sets the target XoopsModule.
	 * 
	 * @access public
	 * @param XoopsModule $xoopsModule
	 */
	function setTargetXoopsModule(&$xoopsModule)
	{
		$this->_mTargetXoopsModule =& $xoopsModule;
		$this->_mTargetVersion = $this->getTargetPhase();
	}
	
	/**
	 * Execute upgrade. If the specific method for the milestone, this method
	 * calls the method. If such milestone doesn't exist, call the automatic
	 * upgrade method.
	 * 
	 * @access public
	 */
	function executeUpgrade()
	{
		if ($this->hasUpgradeMethod()) {
			return $this->_callUpgradeMethod();
		}
		else {
			return $this->executeAutomaticUpgrade();
		}
	}

	/**
	 * Gets the current version.
	 * 
	 * @return int
	 */
	function getCurrentVersion()
	{
		return $this->_mCurrentVersion;
	}
	
	/**
	 * Gets the target varsion number at this time. In the case where there are
	 * milestones, gets the nearest value from the current version.
	 * 
	 * Of course, this class is good to override by the sub-class.
	 */
	function getTargetPhase()
	{
		ksort($this->_mMilestone);
		
		foreach ($this->_mMilestone as $t_version => $t_value) {
			if ($t_version > $this->getCurrentVersion()) {
				return $t_version;
			}
		}
		
		return $this->_mTargetXoopsModule->get('version');
	}
	
	/**
	 * Gets the valude indicating whether this class 
	 */
	function hasUpgradeMethod()
	{
		ksort($this->_mMilestone);
		
		foreach ($this->_mMilestone as $t_version => $t_value) {
			if ($t_version >= $this->getCurrentVersion()) {
				if (is_callable(array($this, $t_value))) {
					return true;
				}
			}
		}
		
		return false;
	}
	
	/**
	 * Dispatches the callback upgrade program.
	 * 
	 * @access protected
	 * @return bool The value indicating whether this method can call the
	 *              upgrade-method.
	 */
	function _callUpgradeMethod()
	{
		ksort($this->_mMilestone);
		
		foreach ($this->_mMilestone as $t_version => $t_value) {
			if ($t_version > $this->getCurrentVersion()) {
				if (is_callable(array($this, $t_value))) {
					return $this->$t_value();
				}
			}
		}
		
		return false;
	}
	
	/**
	 * Gets a valude indicating whether this process is upgrade for the latest
	 * version.
	 * 
	 * @return bool
	 */
	function isLatestUpgrade()
	{
		return ($this->_mTargetXoopsModule->get('version') == $this->getTargetPhase());
	}
	
	/**
	 * Saves XoopsModule object to DB.
	 * 
	 * @access protected
	 */	
	function saveXoopsModule(&$module)
	{
		$handler =& xoops_gethandler('module');
		if ($handler->insert($module)) {
			$this->mLog->addReport("XoopsModule is updated.");
		}
		else {
			$this->mLog->addError("*Could not install module information*");
		}
	}
	
	function _processScript()
	{
		$installScript = trim($this->_mTargetXoopsModule->getInfo('onUpdate'));
		if ($installScript != false) {
			require_once XOOPS_MODULE_PATH . "/" . $this->_mTargetXoopsModule->get('dirname') . "/" . $installScript;
			$funcName = 'xoops_module_update_' . $this->_mTargetXoopsModule->get('dirname');
			if (function_exists($funcName)) {
				if (!call_user_func($funcName, $this->_mTargetXoopsModule, $this->getCurrentVersion())) {
					$this->mLog->addError("Failed to execute " . $funcName);
				}
			}
		}
	}
	
	function _processReport()
	{
		if (!$this->mLog->hasError()) {
			$this->mLog->add(XCube_Utils::formatMessage(_AD_LEGACY_MESSAGE_UPDATING_MODULE_SUCCESSFUL, $this->_mCurrentXoopsModule->get('name')));
		}
		else {
			$this->mLog->addError(XCube_Utils::formatMessage(_AD_LEGACY_ERROR_UPDATING_MODULE_FAILURE, $this->_mCurrentXoopsModule->get('name')));
		}
	}
	
	/**
	 * Updates all of module templates.
	 * 
	 * @access protected
	 * @note You may do custom
	 */
	function _updateModuleTemplates()
	{
		Legacy_ModuleInstallUtils::uninstallAllOfModuleTemplates($this->_mTargetXoopsModule, $this->mLog);
		Legacy_ModuleInstallUtils::installAllOfModuleTemplates($this->_mTargetXoopsModule, $this->mLog);
	}
	
	/**
	 * Updates all of blocks.
	 * 
	 * @access protected
	 * @note You may do custom
	 */
	function _updateBlocks()
	{
		Legacy_ModuleInstallUtils::uninstallAllOfBlocks($this->_mTargetXoopsModule, $this->mLog);
		Legacy_ModuleInstallUtils::installAllOfBlocks($this->_mTargetXoopsModule, $this->mLog);
	}
	
	/**
	 * Updates all of preferences & notifications.
	 * 
	 * @access protected
	 * @note You may do custom
	 */
	function _updatePreferences()
	{
		$this->_upgradePreferences();
	}
	
	/**
	 * This method executes upgrading automatically by the diff of
	 * xoops_version.
	 * 
	 * 1) Uninstall all of module templates
	 * 2) Install all of module templates
	 * 
	 * @return bool
	 */
	function executeAutomaticUpgrade()
	{
		$this->mLog->addReport(_AD_LEGACY_MESSAGE_UPDATE_STARTED);
		
		//
		// Updates all of module templates
		//
		$this->_updateModuleTemplates();
		if (!$this->_mForceMode && $this->mLog->hasError()) {
			$this->_processReport();
			return false;
		}
		
		//
		// Update blocks.
		//
		$this->_updateBlocks();
		if (!$this->_mForceMode && $this->mLog->hasError()) {
			$this->_processReport();
			return false;
		}
		
		//
		// Update preferences & notifications.
		//
		$this->_updatePreferences();
		if (!$this->_mForceMode && $this->mLog->hasError()) {
			$this->_processReport();
			return false;
		}
		
		//
		// Update module object.
		//
		$this->saveXoopsModule($this->_mTargetXoopsModule);
		if (!$this->_mForceMode && $this->mLog->hasError()) {
			$this->_processReport();
			return false;
		}

		//
		// call bacl 'onUpdate'
		//
		$this->_processScript();
		
		$this->_processReport();
		
		return true;
	}

	/**
	 * Tries to smart update all of preferences & notifications.
	 * 
	 * @todo This method is perhaps uncompleted. If you have written more smart
	 *       function, please contribute it to us.
	 * @note The utility method for the custom-update-installer.
	 */
	function _upgradePreferences()
	{
		$configHandler =& xoops_gethandler('config');
		
		//
		// At the start, load config items of this time.
		//
		$criteria =& new CriteriaCompo();
		$criteria->add(new Criteria('conf_modid', $this->_mTargetXoopsModule->get('mid')));
		$criteria->add(new Criteria('conf_catid', 0));
		
		$activeConfigArr =& $configHandler->getConfigs($criteria);
		
		//
		// Next, load config item from xoops_version.php.
		//
		$newConfigArr = array();
		$t_configInfoArr = Legacy_ModuleInstallUtils::getConfigInfosFromManifesto($this->_mTargetXoopsModule);
		
		$count = 0;
		if (is_array($t_configInfoArr)) {
			foreach ($t_configInfoArr as $t_configInfo) {
				$config =& $configHandler->createConfig();
				$config->loadFromConfigInfo($this->_mTargetXoopsModule->get('mid'), $t_configInfo, $count++);
				$newConfigArr[] =& $config;
				unset($config);
			}
		}
		
		//
		// If some active configs were deleted, remove them form database.
		//
		foreach (array_keys($activeConfigArr) as $t_actkey) {
			$findFlag = false;
			foreach (array_keys($newConfigArr) as $t_newkey) {
				
				if ($activeConfigArr[$t_actkey]->get('conf_name') == $newConfigArr[$t_newkey]->get('conf_name')) {
					$findFlag = true;
					if (!$activeConfigArr[$t_actkey]->isEqual($newConfigArr[$t_newkey])) {
						//
						// Update object
						//
						$activeConfigArr[$t_actkey]->set('conf_title', $newConfigArr[$t_newkey]->get('conf_title'));
						$activeConfigArr[$t_actkey]->set('conf_value', $newConfigArr[$t_newkey]->get('conf_value'));
						$activeConfigArr[$t_actkey]->set('conf_desc', $newConfigArr[$t_newkey]->get('conf_desc'));
						$activeConfigArr[$t_actkey]->set('conf_formtype', $newConfigArr[$t_newkey]->get('conf_formtype'));
						$activeConfigArr[$t_actkey]->set('conf_valuetype', $newConfigArr[$t_newkey]->get('conf_valuetype'));
						
						//
						// Delete config options.
						//
						$t_optionArr =& $activeConfigArr[$t_actkey]->getOptionItems();
						foreach (array_keys($t_optionArr) as $t_optionKey) {
							$configHandler->_oHandler->delete($t_optionArr[$t_optionKey]);	//< Exception!!
						}
						
						$activeConfigArr[$t_actkey]->setConfOptions($newConfigArr[$t_newkey]->getConfOptions());
						
						$configHandler->insertConfig($activeConfigArr[$t_actkey]);	//< FIXME need log.
					}
				}
			}
			
			if (!$findFlag) {
				$configHandler->deleteConfig($activeConfigArr[$t_actkey]);
				unset($activeConfigArr[$t_actkey]);
			}
		}
		
		//
		// If some new configs were registered, insert them into database.
		//
		$maxOrder = 0;
		foreach (array_keys($activeConfigArr) as $t_actkey) {
			if ($activeConfigArr[$t_actkey]->get('conf_order') > $maxOrder) {
				$maxOrder = $activeConfigArr[$t_actkey]->get('conf_order');
			}
		}
		
		$order = $maxOrder + 1;
		
		foreach (array_keys($newConfigArr) as $t_newkey) {
			$newFlag = true;
			foreach (array_keys($activeConfigArr) as $t_actkey) {
				if ($newConfigArr[$t_newkey]->isEqual($activeConfigArr[$t_actkey])) {
					$newFlag = false;
					break;
				}
			}
			
			if ($newFlag) {
				$newConfigArr[$t_newkey]->set('conf_order', $order);
				if ($configHandler->insertConfig($newConfigArr[$t_newkey])) {
					$this->mLog->addReport(XCube_Utils::formatMessage(_AD_LEGACY_MESSAGE_INSERT_CONFIG, $newConfigArr[$t_newkey]->get('conf_name')));
				}
				else {
					$this->mLog->addError(XCube_Utils::formatMessage(_AD_LEGACY_ERROR_COULD_NOT_INSERT_CONFIG, $newConfigArr[$t_newkey]->get('conf_name')));
				}
			}
		}
	}
}

?>