<?php
/**
* @version	$Id: factory.php 16421 2016-11-14 07:36:48Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

defined('FULL_PATH') or die('restricted access!');

class kFactory extends kBase implements kiCacheable
{

	/**
	 * Mapping between class name and file name where class definition can be found.
	 *
	 * File path absolute!
	 *
	 * @var array
	 */
	protected $classMap = array();

	/**
	 * Map class names to their pseudo class names, e.g. key=pseudo_class, value=real_class_name.
	 *
	 * @var array
	 */
	protected $realClasses = array();

	/**
	 * Where all created objects are stored.
	 *
	 * @var kBase[]
	 */
	protected $storage = array();

	/**
	 * Debug factory internal processing.
	 *
	 * @var boolean
	 */
	private $_debugFactory = false;

	/**
	 * Profile memory usage during class file inclusion.
	 *
	 * @var boolean
	 */
	private $_profileMemory = false;

	/**
	 * Creates factory instance.
	 */
	public function __construct()
	{
		parent::__construct();

		spl_autoload_register(array($this, 'autoload'), true, true);

		if ( defined('DEBUG_MODE') && DEBUG_MODE && $this->Application->isDebugMode() ) {
			$this->_debugFactory = defined('DBG_FACTORY') && DBG_FACTORY;
			$this->_profileMemory = defined('DBG_PROFILE_MEMORY') && DBG_PROFILE_MEMORY;
		}
	}

	/**
	 * Sets data from cache to object.
	 *
	 * @param array $data Data.
	 *
	 * @return void
	 */
	public function setFromCache(&$data)
	{
		$this->classMap = $data['Factory.Files'];
		$this->realClasses = $data['Factory.realClasses'];
	}

	/**
	 * Performs automatic loading of classes registered with the factory.
	 *
	 * @param string $class Class.
	 *
	 * @return boolean|null
	 */
	public function autoload($class)
	{
		$file = $this->findFile($class);

		if ( $file ) {
			kUtil::includeOnce(FULL_PATH . $file);

			return true;
		}

		return null;
	}

	/**
	 * Finds the absolute path to the file where the class is defined.
	 *
	 * @param string $class The name of the class.
	 *
	 * @return string|false
	 */
	public function findClassFile($class)
	{
		$file = $this->findFile($class);

		if ( $file ) {
			return FULL_PATH . $file;
		}

		return false;
	}

	/**
	 * Finds the path to the file where the class is defined.
	 *
	 * @param string $class The name of the class.
	 *
	 * @return string|boolean The path if found, false otherwise.
	 */
	protected function findFile($class)
	{
		if ( $class[0] == '\\' ) {
			$class = substr($class, 1);
		}

		if ( isset($this->classMap[$class]) ) {
			return $this->classMap[$class];
		}

		$pos = strrpos($class, '\\');

		if ( $pos !== false ) {
			// Namespaced class name.
			$class_path = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR;
			$class_name = substr($class, $pos + 1);
		}
		else {
			// PEAR-like class name.
			$class_path = null;
			$class_name = $class;
		}

		$class_path .= str_replace('_', DIRECTORY_SEPARATOR, $class_name) . '.php';

		foreach ($this->Application->ModuleInfo as $module_name => $module_info) {
			if ( $module_name == 'In-Portal' ) {
				continue;
			}

			if ( strpos($class, $module_info['ClassNamespace']) === 0 ) {
				$test_class_path = str_replace(
					str_replace('\\', DIRECTORY_SEPARATOR, $module_info['ClassNamespace']),
					rtrim($module_info['Path'], '/'),
					$class_path
				);

				if ( file_exists(FULL_PATH . DIRECTORY_SEPARATOR . $test_class_path) ) {
					return DIRECTORY_SEPARATOR . $test_class_path;
				}
			}
		}

		return $this->classMap[$class] = false;
	}

	/**
	 * Gets object data for caching.
	 *
	 * @return array
	 */
	public function getToCache()
	{
		return array(
			'Factory.Files' => $this->classMap,
			'Factory.realClasses' => $this->realClasses,
		);
	}

	/**
	 * Splits any mixing of prefix and special into correct ones.
	 *
	 * @param string $prefix_special Prefix-special.
	 *
	 * @return array
	 */
	public function processPrefix($prefix_special)
	{
		// Example: "l.pick", "l", "m.test_TagProcessor".
		$tmp = explode('_', $prefix_special, 2);
		$tmp[0] = explode('.', $tmp[0]);

		$prefix = $tmp[0][0];
		$prefix_special = $prefix;

		if ( isset($tmp[1]) ) {
			$prefix .= '_' . $tmp[1];
		}

		$special = isset($tmp[0][1]) ? $tmp[0][1] : '';
		$prefix_special .= '.' . $special;

		return array('prefix' => $prefix, 'special' => $special, 'prefix_special' => $prefix_special);
	}


	/**
	 * Returns object using params specified, creates it if is required
	 *
	 * @param string $name
	 * @param string $pseudo_class
	 * @param array $event_params Event params.
	 * @param array $arguments Constructor arguments.
	 * @return kBase
	 * @access public
	 * @throws kFactoryException
	 */
	public function getObject($name, $pseudo_class = '', array $event_params = array(), array $arguments = array())
	{
		$name = rtrim($name, '.');

		if ( isset($this->storage[$name]) ) {
			return $this->storage[$name];
		}

		$ret = $this->processPrefix($name);

		if ( !$pseudo_class ) {
			$pseudo_class = $ret['prefix'];
		}

		if ( !isset($this->realClasses[$pseudo_class]) ) {
			$error_msg = 'RealClass not defined for pseudo_class <strong>' . $pseudo_class . '</strong>';

			if ( $this->Application->isInstalled() ) {
				throw new kFactoryException($error_msg);
			}
			else {
				if ( $this->Application->isDebugMode() ) {
					$this->Application->Debugger->appendTrace();
				}

				trigger_error($error_msg, E_USER_WARNING);
			}

			return false;
		}

		if ( $this->_debugFactory ) {
			$this->Application->Debugger->appendHTML(
				'<strong>Creating object:</strong> Pseudo class: ' . $pseudo_class . ' Prefix: ' . $name
			);
			$this->Application->Debugger->appendTrace();
		}

		$this->storage[$name] = $this->makeClass($pseudo_class, $arguments);
		$this->storage[$name]->Init($ret['prefix'], $ret['special']);
		$this->Application->EventManager->runBuildEvent($ret['prefix_special'], $pseudo_class, $event_params);

		return $this->storage[$name];
	}

	/**
	 * Removes object from storage, so next time it could be created from scratch.
	 *
	 * @param string $name Object's name in the Storage.
	 *
	 * @return void
	 */
	public function DestroyObject($name)
	{
		unset($this->storage[$name]);
	}

	/**
	 * Checks if object with prefix passes was already created in factory.
	 *
	 * @param string $name Object pseudo_class, prefix.
	 *
	 * @return boolean
	 */
	public function hasObject($name)
	{
		return isset($this->storage[$name]);
	}

	/**
	 * Get's real class name for pseudo class,
	 * includes class file and creates class
	 * instance.
	 * All parameters except first one are passed to object constuctor
	 * through mediator method makeClass that creates instance of class
	 *
	 * Pattern: Factory Method
	 *
	 * @param string $pseudo_class
	 * @param Array $arguments
	 * @return kBase
	 * @access public
	 */
	public function makeClass($pseudo_class, array $arguments = array())
	{
		$real_class = $this->realClasses[$pseudo_class];

		$mem_before = memory_get_usage();
		$time_before = microtime(true);

		$arguments = (array)$arguments;

		if ( !$arguments ) {
			$object = new $real_class();
		}
		else {
			$reflection = new ReflectionClass($real_class);
			$object = $reflection->newInstanceArgs($arguments);
		}

		if ( $this->_profileMemory ) {
			$mem_after = memory_get_usage();
			$time_after = microtime(true);
			$mem_used = $mem_after - $mem_before;
			$mem_used_formatted = round($mem_used / 1024, 3);
			$time_used = round($time_after - $time_before, 5);

			$this->Application->Debugger->appendHTML(
				'Factory created <b>' . $real_class . '</b> - used ' . $mem_used_formatted . 'Kb time: ' . $time_used
			);
			$this->Application->Debugger->profilerAddTotal('objects', null, $mem_used);
		}

		return $object;
	}

	/**
	 * Registers new class in the factory
	 *
	 * @param string $real_class   Real name of class as in class declaration.
	 * @param string $file         Filename in what $real_class is declared.
	 * @param string $pseudo_class Name under this class object will be accessed using getObject method.
	 *
	 * @return void
	 */
	public function registerClass($real_class, $file, $pseudo_class = null)
	{
		if ( !isset($pseudo_class) ) {
			$pseudo_class = $real_class;
		}

		if ( !isset($this->classMap[$real_class]) ) {
			$this->classMap[$real_class] = preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '', $file, 1);
		}

		$this->realClasses[$pseudo_class] = $real_class;
	}

	/**
	 * Unregisters existing class from factory
	 *
	 * @param string $real_class   Real name of class as in class declaration.
	 * @param string $pseudo_class Name under this class object is accessed using getObject method.
	 *
	 * @return void
	 */
	public function unregisterClass($real_class, $pseudo_class = null)
	{
		unset($this->classMap[$real_class]);
	}

}

class kFactoryException extends Exception
{

}
