<?php
/**
 * @version   $Id: Config.php 10831 2013-05-29 19:32:17Z btowles $
 * @author    RocketTheme http://www.rockettheme.com
 * @copyright Copyright (C) 2007 - 2014 RocketTheme, LLC
 * @license   http://www.gnu.org/licenses/gpl-2.0.html GNU/GPLv2 only
 */
defined('ROKCOMMON') or die;

/**
 *
 */
class RokCommon_Config_Exception extends Exception
{
}


/**
 *
 */
class RokCommon_Config_PathInfo
{
	/**
	 * @var string
	 */
	public $name;
	/**
	 * @var string
	 */
	public $displayName;
}


/**
 *
 */
class RokCommon_Config
{

	/**
	 * @var
	 */
	protected static $parameters;

	/**
	 * @const string
	 */
	const CONTEXT_PREFIX = 'config';

	/**
	 *
	 */
	const ENTRY_SEPERATOR = '_';

	/**
	 *
	 */
	const MODE_BASEDIR = 'basedir';

	/**
	 *
	 */
	const MODE_CHILDDIR = 'childdir';

	/**
	 *
	 */
	const JOINTYPE_MERGE = 'merge';

	/**
	 *
	 */
	const JOINTYPE_OVERRIDE = 'override';


	/**
	 * @const int
	 */
	const ORDERING_DEFAULT = 100;

	/**
	 * @var RokCommon_Platform_Info
	 */
	protected static $platform_info;

	/**
	 * @var RokCommon_XMLElement
	 */
	protected $xml_node;

	/**
	 * @var string[]
	 */
	protected $paths = array();

	/**
	 * @var RokCommon_Config_PathInfo[]
	 */
	protected $path_configs_info = array();

	/**
	 * @var string
	 */
	protected $name;

	/**
	 * The filename the meta entry looks for
	 * @var string
	 */
	protected $filename;

	/** @var  RokCommon_Composite_Context */
	protected $context;

	/** @var string */
	protected $mode = self::MODE_BASEDIR;

	/**
	 * @var
	 */
	protected $jointype = self::JOINTYPE_MERGE;

	/**
	 * @var RokCommon_Config[]
	 */
	protected $subentries = array();

	/**
	 * @var #Fimplode|?
	 */
	protected $identifier;

	/** @var RokCommon_Config */
	protected $parent;


	/** @var string */
	protected $root_file;
	/**
	 * @var
	 */
	protected $joined_xml;

	/**
	 * @var string
	 */
	protected $parent_identifier;


	/** @var RokCommon_Service_Container */
	protected $container;

	/**
	 * @throws \RokCommon_Config_Exception
	 */
	protected function process()
	{
		/** @var $ret array */
		$ret = array();

		if ($this->context) {
			if ($this->mode == self::MODE_BASEDIR) {
				$ret = $this->context->getAll($this->filename);
			} elseif ($this->mode == self::MODE_CHILDDIR) {
				$ret = $this->context->getAllSubFiles($this->filename);
			} else {
				throw new RokCommon_Config_Exception(rc__('Unknown mode of %s on config', $this->mode));
			}

			ksort($ret, SORT_NUMERIC);

			$parameters = new RokCommon_Registry();
			foreach ($ret as $priority => $files) {
				foreach ($files as $file) {
					$file_xml = RokCommon_Utils_XMLHelper::getXML($file, true);
					if (!isset($file_xml['name'])) {
						throw new RokCommon_Config_Exception(rc__('No "name" attribute defined on config %s', $file));
					}
					// add the infomation to the container
					$param_path   = explode('_', $this->identifier);
					$param_path[] = (string)$file_xml['name'];
					self::$parameters->set(implode('.', $param_path) . '.name', (string)$file_xml['name']);
					self::$parameters->set(implode('.', $param_path) . '.display_name', (isset($file_xml['displayName'])) ? (string)$file_xml['displayName'] : $file_xml['name']);
					self::$parameters->set(implode('.', $param_path) . '.config_file', $file);

					$this->container->addParameters(self::$parameters->toArray());
					self::mergeNodes($this->joined_xml, $file_xml);
				}
			}

			foreach ($this->subentries as $subentry) {
				if ($this->jointype == self::JOINTYPE_MERGE) {
					self::mergeNodes($this->joined_xml, $subentry->get());
				} else {
					throw new RokCommon_Config_Exception(rc__('Unknown Join Type of %s on config', $this->jointype));
				}
			}

		}
		//self::reorderNodes($this->joined_xml);
	}

	/**
	 * @return \RokCommon_XMLElement
	 */
	public function get()
	{
		return $this->joined_xml;
	}


	/**
	 * @param $identifier
	 *
	 * @return \RokCommon_Config_PathInfo[]
	 */
	public function getPathConfigsInfo($identifier)
	{
		if ($this->identifier == $identifier) {
			return $this->path_configs_info;
		} else {
			$ret = array();
			foreach ($this->subentries as $subentry) {
				$ret = $subentry->getPathConfigsInfo($identifier);
				if (!empty($ret)) {
					break;
				}
			}
			return $ret;
		}
	}

	/**
	 * Get an instance of config
	 * @static
	 *
	 * @param $metaconfig_path
	 *
	 * @return \RokCommon_Config
	 * @throws \RokCommon_Config_Exception
	 */
	public static function &getInstance($metaconfig_path)
	{
		if (!isset(self::$parameters)) {
			self::$parameters = new RokCommon_Registry();
		}
		if (!file_exists($metaconfig_path) || !is_file($metaconfig_path) || !is_readable($metaconfig_path)) {
			throw new RokCommon_Config_Exception(rc__('Unable to read file %s', $metaconfig_path));
		}
		$config = new self(self::CONTEXT_PREFIX, $metaconfig_path);
		return $config;
	}

	/**
	 * @param      $parent_identifier
	 * @param      $file
	 *
	 * @param null $xml
	 *
	 * @throws \RokCommon_Config_Exception
	 */
	protected function __construct($parent_identifier, $file = null, $xml = null)
	{
		$this->parent_identifier = $parent_identifier;

		$this->joined_xml = new RokCommon_XMLElement('<config/>');
		$this->container  = RokCommon_Service::getContainer();
		$this->root_file  = $file;
		if (empty($xml)) {
			$xml = RokCommon_Utils_XMLHelper::getXML($file);
		}

		$this->initialize($xml);
		$this->process();
	}

	/**
	 * @param \RokCommon_XMLElement $xml_node
	 *
	 * @throws \RokCommon_Config_Exception
	 */
	protected function initialize(RokCommon_XMLElement $xml_node)
	{
		$this->xml_node = $xml_node;

		// get the name of the entry
		if (!isset($this->xml_node['name'])) {
			throw new RokCommon_Config_Exception(rc__('Meta Config entry in %s does not have a name', $this->parent_identifier));
		}
		$this->name = (string)$this->xml_node['name'];


		// set the identifier name
		$id_parts         = explode(self::ENTRY_SEPERATOR, $this->parent_identifier);
		$id_parts[]       = $this->name;
		$this->identifier = implode(self::ENTRY_SEPERATOR, $id_parts);

		// get the filename of the entry
		if (!isset($this->xml_node['filename'])) {
			throw new RokCommon_Config_Exception(rc__('Meta Config entry %s does not have a filename', $this->identifier));
		}
		$this->filename = (string)$this->xml_node['filename'];


		// get the mode
		if (isset($this->xml_node['mode'])) {
			$this->mode = (string)$this->xml_node['mode'];
		}

		// get the jointype
		if (isset($this->xml_node['jointype'])) {
			$this->jointype = (string)$this->xml_node['jointype'];
		}

		// see if there is a library and add it to the lib path
		$library_paths = $xml_node->xpath('libraries/library');
		if ($library_paths) {
			foreach ($library_paths as $library_path) {
				$resolved_lib_path = RokCommon_Config::replaceTokens((string)$library_path, dirname($this->root_file));
				if (is_dir($resolved_lib_path)) {
					RokCommon_ClassLoader::addPath($resolved_lib_path);
				}
			}
		}

		// get the paths for the config
		$paths = $xml_node->xpath('paths/path');
		if (!$paths) {
			throw new RokCommon_Config_Exception(rc__('Meta Config entry %s must have at least one path.', $this->identifier));
		}

		foreach ($paths as $path_entry) {
			$priority = RokCommon_Composite::DEFAULT_PRIORITY;
			if (isset($path_entry['priority'])) {
				$priority = (string)$path_entry['priority'];
			}
			$path = RokCommon_Config::replaceTokens((string)$path_entry, dirname($this->root_file));
			if (is_dir($path)) {
				// see if there is a testservice entry
				if (isset($path_entry['testservice'])) {
					// see if the testservice extists
					$testservice_name = (string)$path_entry['testservice'];
					$container        = RokCommon_Service::getContainer();

					/** @var $testservice RokCommon_Config_PathTest */
					$testservice = $container->$testservice_name;
					if (!$container->hasService($testservice_name)) {
						throw new RokCommon_Config_Exception(rc__('Path test service %s does not exist', $testservice_name));
					}
					// see if we can add the
					if ($testservice->isPathAvailable()) {
						$this->addPath($path, $priority);
					}
				} else {
					// add the path if there is no testclass
					$this->addPath($path, $priority);
				}
			} else {
				// TODO log unable to find path
			}
		}

		// add any subconfigs
		$subconfigs = $xml_node->xpath('subconfigs/subconfig');
		if ($subconfigs) {
			foreach ($subconfigs as $subconfig_entry) {
				$subconfig                               = new self($this->identifier, $this->root_file, $subconfig_entry);
				$this->subentries[$subconfig->getName()] = $subconfig;
			}
		}

		$this->context = RokCommon_Composite::get($this->identifier);
	}

	/**
	 * @param \RokCommon_Composite_Context $context
	 */
	public function setContext($context)
	{
		$this->context = $context;
	}

	/**
	 * @return \RokCommon_Composite_Context
	 */
	public function getContext()
	{
		return $this->context;
	}

	/**
	 * @param string $filename
	 */
	public function setFilename($filename)
	{
		$this->filename = $filename;
	}

	/**
	 * @return string
	 */
	public function getFilename()
	{
		return $this->filename;
	}

	/**
	 * @param  $identifier
	 */
	public function setIdentifier($identifier)
	{
		$this->identifier = $identifier;
	}

	/**
	 * @return
	 */
	public function getIdentifier()
	{
		return $this->identifier;
	}

	/**
	 * @param  $jointype
	 */
	public function setJointype($jointype)
	{
		$this->jointype = $jointype;
	}

	/**
	 * @return string
	 */
	public function getJointype()
	{
		return $this->jointype;
	}

	/**
	 * @param string $mode
	 */
	public function setMode($mode)
	{
		$this->mode = $mode;
	}

	/**
	 * @return string
	 */
	public function getMode()
	{
		return $this->mode;
	}

	/**
	 * @param string $name
	 */
	public function setName($name)
	{
		$this->name = $name;
	}

	/**
	 * @return string
	 */
	public function getName()
	{
		return $this->name;
	}

	/**
	 * @param \RokCommon_Config $parent
	 */
	public function setParent($parent)
	{
		$this->parent = $parent;
	}

	/**
	 * @return \RokCommon_Config
	 */
	public function getParent()
	{
		return $this->parent;
	}

	/**
	 * @param array $paths
	 */
	public function setPaths($paths)
	{
		$this->paths = $paths;
	}

	/**
	 * @return array
	 */
	public function getPaths()
	{
		return $this->paths;
	}

	/**
	 * @param array $subentries
	 */
	public function setSubentries($subentries)
	{
		$this->subentries = $subentries;
	}

	/**
	 * @return array
	 */
	public function getSubentries()
	{
		return $this->subentries;
	}

	/**
	 * @param \RokCommon_XMLElement $xml_node
	 */
	public function setXmlNode($xml_node)
	{
		$this->xml_node = $xml_node;
	}

	/**
	 * @return \RokCommon_XMLElement
	 */
	public function getXmlNode()
	{
		return $this->xml_node;
	}

	/**
	 * @param string $path
	 * @param int    $priority
	 */
	public function addPath($path, $priority = RokCommon_Composite::DEFAULT_PRIORITY)
	{
		//self::addConfigPath($this->identifier, $path, $priority);
		RokCommon_Composite::addPackagePath($this->identifier, $path, $priority);
	}


	/**
	 * @static
	 *
	 * @param string $extension
	 * @param string $path
	 * @param int    $priority
	 */
	public static function addConfigPath($extension, $path, $priority = RokCommon_Composite::DEFAULT_PRIORITY)
	{

	}


	/**
	 * Adds a new child SimpleXMLElement node to the source.
	 *
	 * @param   SimpleXMLElement  $source  The source element on which to append.
	 * @param   SimpleXMLElement  $new     The new element to append.
	 *
	 * @return  void
	 *
	 * @since   11.1
	 * @throws  Exception if an error occurs.
	 */
	protected static function addNode(SimpleXMLElement $source, SimpleXMLElement $new)
	{
		// Add the new child node.
		$node = $source->addChild($new->getName(), trim($new));

		// Add the attributes of the child node.
		foreach ($new->attributes() as $name => $value) {
			$node->addAttribute($name, $value);
		}

		// Add any children of the new node.
		foreach ($new->children() as $child) {
			self::addNode($node, $child);
		}
	}

	/**
	 * Adds a new child SimpleXMLElement node to the source.
	 *
	 * @param   SimpleXMLElement  $source  The source element on which to append.
	 * @param   SimpleXMLElement  $new     The new element to append.
	 *
	 * @return  void
	 *
	 * @since   11.1
	 */
	protected static function mergeNode(SimpleXMLElement $source, SimpleXMLElement $new)
	{
		// Update the attributes of the child node.
		foreach ($new->attributes() as $name => $value) {
			if (isset($source[$name])) {
				$source[$name] = (string)$value;
			} else {
				$source->addAttribute($name, $value);
			}
		}

		// What to do with child elements?
	}

	/**
	 * Merges new elements into a source <fields> element.
	 *
	 * @param   SimpleXMLElement  $source  The source element.
	 * @param   SimpleXMLElement  $new     The new element to merge.
	 *
	 * @return  void
	 *
	 * @since   11.1
	 */
	protected static function mergeNodes(SimpleXMLElement &$source, SimpleXMLElement $new)
	{
		// The assumption is that the inputs are at the same relative level.
		// So we just have to scan the children and deal with them.

		// Update the attributes of the child node.
		foreach ($new->attributes() as $name => $value) {
			if (isset($source[$name])) {
				$source[$name] = (string)$value;
			} else {
				$source->addAttribute($name, $value);
			}
		}


		/** @var $child SimpleXMLElement */
		foreach ($new->children() as $child) {
			$type = $child->getName();
			$name = $child['name'];

			// Does this node exist?
			$fields = $source->xpath($type . '[@name="' . $name . '"]');

			if (empty($fields)) {
				// This node does not exist, so add it.
				self::addNode($source, $child);
			} else {
				// This node does exist.
				switch ($type) {
					case 'field':
						self::mergeNode($fields[0], $child);
						break;

					default:
						self::mergeNodes($fields[0], $child);
						break;
				}
			}
		}
	}

	/**
	 * @static
	 *
	 * @param \SimpleXMLElement $node
	 *
	 * @return array|\SimpleXMLElement
	 */
	protected static function reorderNodes(SimpleXMLElement &$node)
	{

		$reordered_node = new SimpleXMLElement('<' . $node->getName() . '/>');
		foreach ($node->attributes() as $name => $value) {
			if (isset($node[$name])) {
				$reordered_node[$name] = (string)$value;
			} else {
				$reordered_node->addAttribute($name, $value);
			}
		}

		/** @var $child SimpleXMLElement */
		foreach ($node->children() as $child) {
			$type = $child->getName();
			switch ($type) {
				case 'fieldset':
					$reordered_child = self::reorderNode($child);
					break;
				default:
					$reordered_child = self::reorderNodes($child);
					break;
			}
			$reordered_node->addChild($type, $reordered_child);


		}

		return $reordered_node;
	}

	/**
	 * @static
	 *
	 * @param SimpleXMLElement $node
	 *
	 * @return array|SimpleXMLElement
	 */
	protected static function reorderNode(SimpleXMLElement &$node)
	{
		$reordered_node = new SimpleXMLElement('<' . $node->getName() . '/>');
		foreach ($node->attributes() as $name => $value) {
			if (isset($node[$name])) {
				$reordered_node[$name] = (string)$value;
			} else {
				$reordered_node->addAttribute($name, $value);
			}
		}

		/** @var $fieldArray SimpleXMLElement[] */
		$fieldArray = array();
		foreach ($node->field as $d) {
			$fieldArray[] = $d;
		}
		usort($fieldArray, array('RokCommon_Config', 'sortXmlNodesByConfigOrdering'));
		foreach ($fieldArray as $ordered_sub_node) {
			$reordered_node->addChild('field', $ordered_sub_node);
		}
		return $reordered_node;
	}

	/**
	 * @static
	 *
	 * @param $a
	 * @param $b
	 *
	 * @return int
	 */
	protected static function sortXmlNodesByConfigOrdering($a, $b)
	{
		$va = (isset($a['configordering'])) ? (int)$a['configordering'] : self::ORDERING_DEFAULT;
		$vb = (isset($b['configordering'])) ? (int)$b['configordering'] : self::ORDERING_DEFAULT;
		if ($va === $vb) {
			return 0;
		}
		return ($va < $vb) ? -1 : 1;
	}


	/**
	 * @param $string
	 *
	 * @param $file_directory
	 *
	 * @return mixed
	 */
	public static function replaceTokens($string, $file_directory)
	{
		$contianer = RokCommon_Service::getContainer();
		/** @var $platform_info RokCommon_IPlatformInfo */
		$platform_info = $contianer->platforminfo;
		$string        = RokCommon_Template::replace('CURRENT_PATH', $file_directory, $string);
		$string        = RokCommon_Template::replace('ROOT_PATH', $platform_info->getRootPath(), $string);
		$string        = RokCommon_Template::replace('TEMPLATE_PATH', $platform_info->getDefaultTemplatePath(), $string);
		return $string;
	}

	/**
	 * @param string $parent_identifier
	 */
	public function setParentIdentifier($parent_identifier)
	{
		$this->parent_identifier = $parent_identifier;
	}

	/**
	 * @return string
	 */
	public function getParentIdentifier()
	{
		return $this->parent_identifier;
	}
}
